mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-18 10:07:02 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
425a1349c6 | ||
|
|
0f1e75b473 | ||
|
|
d80521b24d | ||
|
|
ef48b9c2dd | ||
|
|
6b606ebb9b | ||
|
|
962e995268 | ||
|
|
a0a5a9acb0 | ||
|
|
596b268380 | ||
|
|
193a5f6f13 | ||
|
|
21ad7a2452 | ||
|
|
bd4db645bb | ||
|
|
ba943cb682 | ||
|
|
b2c3c4ce0f | ||
|
|
038f4fafd3 | ||
|
|
f34a73f8a6 | ||
|
|
e5b6ed2230 | ||
|
|
38c124875e | ||
|
|
00243a622f | ||
|
|
5ccd6b2044 | ||
|
|
ea6bb44969 | ||
|
|
483ddc1c5e | ||
|
|
47cba08c33 |
@@ -8,6 +8,6 @@
|
||||
"Include": "README*,LICENSE*,inbucket.bat,etc,themes"
|
||||
},
|
||||
"PackageVersion": "1.0",
|
||||
"PrereleaseInfo": "rc1",
|
||||
"PrereleaseInfo": "rc2",
|
||||
"FormatVersion": "0.8"
|
||||
}
|
||||
|
||||
9
.travis.yml
Normal file
9
.travis.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.1
|
||||
- tip
|
||||
|
||||
install:
|
||||
- go get -v ./...
|
||||
- go get github.com/stretchr/testify
|
||||
@@ -1,4 +1,4 @@
|
||||
Inbucket
|
||||
Inbucket [](https://travis-ci.org/jhillyerd/inbucket)
|
||||
========
|
||||
|
||||
Inbucket is an email testing service; it will accept messages for any email
|
||||
@@ -21,7 +21,7 @@ Read more at the [Inbucket website](http://jhillyerd.github.io/inbucket/).
|
||||
Development Status
|
||||
------------------
|
||||
|
||||
Inbucket is currently beta quality: it works but is not well tested.
|
||||
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)
|
||||
for more details.
|
||||
|
||||
@@ -94,6 +94,9 @@ func main() {
|
||||
fmt.Fprintf(pidf, "%v\n", os.Getpid())
|
||||
}
|
||||
|
||||
// Grab our datastore
|
||||
ds := smtpd.DefaultFileDataStore()
|
||||
|
||||
// Start HTTP server
|
||||
go web.Start()
|
||||
|
||||
@@ -102,7 +105,7 @@ func main() {
|
||||
go pop3Server.Start()
|
||||
|
||||
// Startup SMTP server, block until it exits
|
||||
smtpServer = smtpd.New()
|
||||
smtpServer = smtpd.NewSmtpServer(config.GetSmtpConfig(), ds)
|
||||
smtpServer.Start()
|
||||
|
||||
// Wait for active connections to finish
|
||||
|
||||
@@ -611,21 +611,21 @@ func (ses *Session) ooSeq(cmd string) {
|
||||
|
||||
// Session specific logging methods
|
||||
func (ses *Session) logTrace(msg string, args ...interface{}) {
|
||||
log.LogTrace("POP3<%v> %v", ses.id, fmt.Sprintf(msg, args...))
|
||||
log.LogTrace("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
func (ses *Session) logInfo(msg string, args ...interface{}) {
|
||||
log.LogInfo("POP3<%v> %v", ses.id, fmt.Sprintf(msg, args...))
|
||||
log.LogInfo("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
func (ses *Session) logWarn(msg string, args ...interface{}) {
|
||||
// Update metrics
|
||||
//expWarnsTotal.Add(1)
|
||||
log.LogWarn("POP3<%v> %v", ses.id, fmt.Sprintf(msg, args...))
|
||||
log.LogWarn("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
func (ses *Session) logError(msg string, args ...interface{}) {
|
||||
// Update metrics
|
||||
//expErrorsTotal.Add(1)
|
||||
log.LogError("POP3<%v> %v", ses.id, fmt.Sprintf(msg, args...))
|
||||
log.LogError("POP3[%v]<%v> %v", ses.remoteHost, ses.id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
@@ -76,7 +76,10 @@ func DefaultFileDataStore() DataStore {
|
||||
// Retrieves the Mailbox object for a specified email address, if the mailbox
|
||||
// does not exist, it will attempt to create it.
|
||||
func (ds *FileDataStore) MailboxFor(emailAddress string) (Mailbox, error) {
|
||||
name := ParseMailboxName(emailAddress)
|
||||
name, err := ParseMailboxName(emailAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir := HashMailboxName(name)
|
||||
s1 := dir[0:3]
|
||||
s2 := dir[0:6]
|
||||
@@ -339,10 +342,10 @@ func (m *FileMessage) ReadHeader() (msg *mail.Message, err error) {
|
||||
// ReadBody opens the .raw portion of a Message and returns a MIMEBody object
|
||||
func (m *FileMessage) ReadBody() (body *enmime.MIMEBody, 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)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -12,7 +15,7 @@ import (
|
||||
|
||||
// Test directory structure created by filestore
|
||||
func TestFSDirStructure(t *testing.T) {
|
||||
ds := setupDataStore()
|
||||
ds, logbuf := setupDataStore()
|
||||
defer teardownDataStore(ds)
|
||||
root := ds.path
|
||||
|
||||
@@ -85,11 +88,18 @@ func TestFSDirStructure(t *testing.T) {
|
||||
assert.False(t, isPresent(expect), "Did not expect %q to exist", expect)
|
||||
expect = mbPath
|
||||
assert.False(t, isPresent(expect), "Did not expect %q to exist", expect)
|
||||
|
||||
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 FileDataStore.AllMailboxes()
|
||||
func TestFSAllMailboxes(t *testing.T) {
|
||||
ds := setupDataStore()
|
||||
ds, logbuf := setupDataStore()
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
for _, name := range []string{"abby", "bill", "christa", "donald", "evelyn"} {
|
||||
@@ -105,12 +115,19 @@ func TestFSAllMailboxes(t *testing.T) {
|
||||
mboxes, err := ds.AllMailboxes()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(mboxes), 5)
|
||||
|
||||
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, meanwhile querying its
|
||||
// contents with a new mailbox object each time
|
||||
func TestFSDeliverMany(t *testing.T) {
|
||||
ds := setupDataStore()
|
||||
ds, logbuf := setupDataStore()
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "fred"
|
||||
@@ -148,11 +165,18 @@ func TestFSDeliverMany(t *testing.T) {
|
||||
subj := msgs[i].Subject()
|
||||
assert.Equal(t, expect, subj, "Expected subject %q, got %q", expect, subj)
|
||||
}
|
||||
|
||||
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 deleting messages
|
||||
func TestFSDelete(t *testing.T) {
|
||||
ds := setupDataStore()
|
||||
ds, logbuf := setupDataStore()
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "fred"
|
||||
@@ -216,11 +240,17 @@ func TestFSDelete(t *testing.T) {
|
||||
assert.Equal(t, expect, subj, "Expected subject %q, got %q", expect, subj)
|
||||
}
|
||||
|
||||
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 purging a mailbox
|
||||
func TestFSPurge(t *testing.T) {
|
||||
ds := setupDataStore()
|
||||
ds, logbuf := setupDataStore()
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "fred"
|
||||
@@ -257,11 +287,18 @@ func TestFSPurge(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.Equal(t, len(msgs), 0, "Expected mailbox to have zero messages, got %v", 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Test message size calculation
|
||||
func TestFSSize(t *testing.T) {
|
||||
ds := setupDataStore()
|
||||
ds, logbuf := setupDataStore()
|
||||
defer teardownDataStore(ds)
|
||||
|
||||
mbName := "fred"
|
||||
@@ -288,15 +325,27 @@ func TestFSSize(t *testing.T) {
|
||||
size := msg.Size()
|
||||
assert.Equal(t, expect, size, "Expected size of %v, got %v", expect, size)
|
||||
}
|
||||
|
||||
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 {
|
||||
func setupDataStore() (*FileDataStore, *bytes.Buffer) {
|
||||
path, err := ioutil.TempDir("", "inbucket")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return NewFileDataStore(path).(*FileDataStore)
|
||||
|
||||
// Capture log output
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
|
||||
return NewFileDataStore(path).(*FileDataStore), buf
|
||||
}
|
||||
|
||||
// deliverMessage creates and delivers a message to the specific mailbox, returning
|
||||
|
||||
@@ -211,29 +211,35 @@ func (ss *Session) greetHandler(cmd string, arg string) {
|
||||
// READY state -> waiting for MAIL
|
||||
func (ss *Session) readyHandler(cmd string, arg string) {
|
||||
if cmd == "MAIL" {
|
||||
// (?i) makes the regex case insensitive
|
||||
re := regexp.MustCompile("(?i)^FROM:\\s*<([^>]+)>( [\\w= ]+)?$")
|
||||
// Match FROM, while accepting '>' as quoted pair and in double quoted strings
|
||||
// (?i) makes the regex case insensitive, (?:) is non-grouping sub-match
|
||||
re := regexp.MustCompile("(?i)^FROM:\\s*<((?:\\\\>|[^>])+|\"[^\"]+\"@[^>]+)>( [\\w= ]+)?$")
|
||||
m := re.FindStringSubmatch(arg)
|
||||
if m == nil {
|
||||
ss.send("501 Was expecting MAIL arg syntax of FROM:<address>")
|
||||
ss.logWarn("Bad MAIL argument: '%v'", arg)
|
||||
ss.logWarn("Bad MAIL argument: %q", arg)
|
||||
return
|
||||
}
|
||||
from := m[1]
|
||||
if _, _, err := ParseEmailAddress(from); err != nil {
|
||||
ss.send("501 Bad sender address syntax")
|
||||
ss.logWarn("Bad address as MAIL arg: %q, %s", from, err)
|
||||
return
|
||||
}
|
||||
// This is where the client may put BODY=8BITMIME, but we already
|
||||
// ready the DATA as bytes, so it does not effect our processing.
|
||||
// read the DATA as bytes, so it does not effect our processing.
|
||||
if m[2] != "" {
|
||||
args, ok := ss.parseArgs(m[2])
|
||||
if !ok {
|
||||
ss.send("501 Unable to parse MAIL ESMTP parameters")
|
||||
ss.logWarn("Bad MAIL argument: '%v'", arg)
|
||||
ss.logWarn("Bad MAIL argument: %q", arg)
|
||||
return
|
||||
}
|
||||
if args["SIZE"] != "" {
|
||||
size, err := strconv.ParseInt(args["SIZE"], 10, 32)
|
||||
if err != nil {
|
||||
ss.send("501 Unable to parse SIZE as an integer")
|
||||
ss.logWarn("Unable to parse SIZE '%v' as an integer", args["SIZE"])
|
||||
ss.logWarn("Unable to parse SIZE %q as an integer", args["SIZE"])
|
||||
return
|
||||
}
|
||||
if int(size) > ss.server.maxMessageBytes {
|
||||
@@ -259,11 +265,16 @@ func (ss *Session) mailHandler(cmd string, arg string) {
|
||||
case "RCPT":
|
||||
if (len(arg) < 4) || (strings.ToUpper(arg[0:3]) != "TO:") {
|
||||
ss.send("501 Was expecting RCPT arg syntax of TO:<address>")
|
||||
ss.logWarn("Bad RCPT argument: '%v'", arg)
|
||||
ss.logWarn("Bad RCPT argument: %q", arg)
|
||||
return
|
||||
}
|
||||
// This trim is probably too forgiving
|
||||
recip := strings.Trim(arg[3:], "<> ")
|
||||
if _, _, err := ParseEmailAddress(recip); err != nil {
|
||||
ss.send("501 Bad recipient address syntax")
|
||||
ss.logWarn("Bad address as RCPT arg: %q, %s", recip, err)
|
||||
return
|
||||
}
|
||||
if ss.recipients.Len() >= ss.server.maxRecips {
|
||||
ss.logWarn("Maximum limit of %v recipients reached", ss.server.maxRecips)
|
||||
ss.send(fmt.Sprintf("552 Maximum limit of %v recipients reached", ss.server.maxRecips))
|
||||
@@ -276,7 +287,7 @@ func (ss *Session) mailHandler(cmd string, arg string) {
|
||||
case "DATA":
|
||||
if arg != "" {
|
||||
ss.send("501 DATA command should not have any arguments")
|
||||
ss.logWarn("Got unexpected args on DATA: '%v'", arg)
|
||||
ss.logWarn("Got unexpected args on DATA: %q", arg)
|
||||
return
|
||||
}
|
||||
if ss.recipients.Len() > 0 {
|
||||
@@ -303,19 +314,26 @@ func (ss *Session) dataHandler() {
|
||||
i := 0
|
||||
for e := ss.recipients.Front(); e != nil; e = e.Next() {
|
||||
recip := e.Value.(string)
|
||||
if !strings.HasSuffix(strings.ToLower(recip), "@"+ss.server.domainNoStore) {
|
||||
local, domain, err := ParseEmailAddress(recip)
|
||||
if err != nil {
|
||||
ss.logError("Failed to parse address for %q", recip)
|
||||
ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", recip))
|
||||
ss.reset()
|
||||
return
|
||||
}
|
||||
if strings.ToLower(domain) != ss.server.domainNoStore {
|
||||
// Not our "no store" domain, so store the message
|
||||
mb, err := ss.server.dataStore.MailboxFor(recip)
|
||||
mb, err := ss.server.dataStore.MailboxFor(local)
|
||||
if err != nil {
|
||||
ss.logError("Failed to open mailbox for %v", recip)
|
||||
ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", recip))
|
||||
ss.logError("Failed to open mailbox for %q", local)
|
||||
ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", local))
|
||||
ss.reset()
|
||||
return
|
||||
}
|
||||
mailboxes[i] = mb
|
||||
messages[i] = mb.NewMessage()
|
||||
} else {
|
||||
log.LogTrace("Not storing message for '%v'", recip)
|
||||
log.LogTrace("Not storing message for %q", recip)
|
||||
}
|
||||
i++
|
||||
}
|
||||
@@ -409,7 +427,7 @@ func (ss *Session) send(msg string) {
|
||||
}
|
||||
if _, err := fmt.Fprint(ss.conn, msg+"\r\n"); err != nil {
|
||||
ss.sendError = err
|
||||
ss.logWarn("Failed to send: '%v'", msg)
|
||||
ss.logWarn("Failed to send: %q", msg)
|
||||
return
|
||||
}
|
||||
ss.logTrace(">> %v >>", msg)
|
||||
@@ -462,19 +480,19 @@ func (ss *Session) parseCmd(line string) (cmd string, arg string, ok bool) {
|
||||
case l == 0:
|
||||
return "", "", true
|
||||
case l < 4:
|
||||
ss.logWarn("Command too short: '%v'", line)
|
||||
ss.logWarn("Command too short: %q", line)
|
||||
return "", "", false
|
||||
case l == 4:
|
||||
return strings.ToUpper(line), "", true
|
||||
case l == 5:
|
||||
// Too long to be only command, too short to have args
|
||||
ss.logWarn("Mangled command: '%v'", line)
|
||||
ss.logWarn("Mangled command: %q", line)
|
||||
return "", "", false
|
||||
}
|
||||
// If we made it here, command is long enough to have args
|
||||
if line[4] != ' ' {
|
||||
// There wasn't a space after the command?
|
||||
ss.logWarn("Mangled command: '%v'", line)
|
||||
ss.logWarn("Mangled command: %q", line)
|
||||
return "", "", false
|
||||
}
|
||||
// I'm not sure if we should trim the args or not, but we will for now
|
||||
@@ -491,7 +509,7 @@ func (ss *Session) parseArgs(arg string) (args map[string]string, ok bool) {
|
||||
re := regexp.MustCompile(" (\\w+)=(\\w+)")
|
||||
pm := re.FindAllStringSubmatch(arg, -1)
|
||||
if pm == nil {
|
||||
ss.logWarn("Failed to parse arg string: '%v'")
|
||||
ss.logWarn("Failed to parse arg string: %q")
|
||||
return nil, false
|
||||
}
|
||||
for _, m := range pm {
|
||||
@@ -514,21 +532,21 @@ func (ss *Session) ooSeq(cmd string) {
|
||||
|
||||
// Session specific logging methods
|
||||
func (ss *Session) logTrace(msg string, args ...interface{}) {
|
||||
log.LogTrace("SMTP<%v> %v", ss.id, fmt.Sprintf(msg, args...))
|
||||
log.LogTrace("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
func (ss *Session) logInfo(msg string, args ...interface{}) {
|
||||
log.LogInfo("SMTP<%v> %v", ss.id, fmt.Sprintf(msg, args...))
|
||||
log.LogInfo("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
func (ss *Session) logWarn(msg string, args ...interface{}) {
|
||||
// Update metrics
|
||||
expWarnsTotal.Add(1)
|
||||
log.LogWarn("SMTP<%v> %v", ss.id, fmt.Sprintf(msg, args...))
|
||||
log.LogWarn("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
func (ss *Session) logError(msg string, args ...interface{}) {
|
||||
// Update metrics
|
||||
expErrorsTotal.Add(1)
|
||||
log.LogError("SMTP<%v> %v", ss.id, fmt.Sprintf(msg, args...))
|
||||
log.LogError("SMTP[%v]<%v> %v", ss.remoteHost, ss.id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
380
smtpd/handler_test.go
Normal file
380
smtpd/handler_test.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/jhillyerd/inbucket/config"
|
||||
"io"
|
||||
//"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type scriptStep struct {
|
||||
send string
|
||||
expect int
|
||||
}
|
||||
|
||||
// Test commands in GREET state
|
||||
func TestGreetState(t *testing.T) {
|
||||
// Setup mock objects
|
||||
mds := &MockDataStore{}
|
||||
mb1 := &MockMailbox{}
|
||||
mds.On("MailboxFor").Return(mb1, nil)
|
||||
|
||||
server, logbuf := setupSmtpServer(mds)
|
||||
defer teardownSmtpServer(server)
|
||||
|
||||
var script []scriptStep
|
||||
|
||||
// Test out some mangled HELOs
|
||||
script = []scriptStep{
|
||||
{"HELLO", 500},
|
||||
{"HELL", 500},
|
||||
{"hello", 500},
|
||||
{"Outlook", 500},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
if err := playSession(t, server, []scriptStep{{"HELO mydom.com", 250}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := playSession(t, server, []scriptStep{{"HelO mydom.com", 250}}); err != nil {
|
||||
t.Error(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 commands in READY state
|
||||
func TestReadyState(t *testing.T) {
|
||||
// Setup mock objects
|
||||
mds := &MockDataStore{}
|
||||
mb1 := &MockMailbox{}
|
||||
mds.On("MailboxFor").Return(mb1, nil)
|
||||
|
||||
server, logbuf := setupSmtpServer(mds)
|
||||
defer teardownSmtpServer(server)
|
||||
|
||||
var script []scriptStep
|
||||
|
||||
// Test out some mangled READY commands
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"FOOB", 500},
|
||||
{"HELO", 503},
|
||||
{"DATA", 503},
|
||||
{"MAIL", 501},
|
||||
{"MAIL FROM john@gmail.com", 501},
|
||||
{"MAIL FROM:john@gmail.com", 501},
|
||||
{"MAIL FROM:<john@gmail.com> SIZE=147KB", 501},
|
||||
{"MAIL FROM: <john@gmail.com> SIZE147", 501},
|
||||
{"MAIL FROM:<first@last@gmail.com>", 501},
|
||||
{"MAIL FROM:<first last@gmail.com>", 501},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test out some valid MAIL commands
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM: <john@gmail.com>", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM: <john@gmail.com> BODY=8BITMIME", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<john@gmail.com> SIZE=1024", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<host!host!user/data@foo.com>", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<\"first last\"@space.com>", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<user\\@internal@external.com>", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<user\\>name@host.com>", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<\"user>name\"@host.com>", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<\"user@internal\"@external.com>", 250},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(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 commands in MAIL state
|
||||
func TestMailState(t *testing.T) {
|
||||
// Setup mock objects
|
||||
mds := &MockDataStore{}
|
||||
mb1 := &MockMailbox{}
|
||||
msg1 := &MockMessage{}
|
||||
mds.On("MailboxFor").Return(mb1, nil)
|
||||
mb1.On("NewMessage").Return(msg1)
|
||||
msg1.On("Close").Return(nil)
|
||||
|
||||
server, logbuf := setupSmtpServer(mds)
|
||||
defer teardownSmtpServer(server)
|
||||
|
||||
var script []scriptStep
|
||||
|
||||
// Test out some mangled READY commands
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"FOOB", 500},
|
||||
{"HELO", 503},
|
||||
{"DATA", 503},
|
||||
{"MAIL", 503},
|
||||
{"RCPT", 501},
|
||||
{"RCPT TO", 501},
|
||||
{"RCPT TO james@gmail.com", 501},
|
||||
{"RCPT TO:<first last@host.com>", 501},
|
||||
{"RCPT TO:<fred@fish@host.com", 501},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test out some good RCPT commands
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<u1@gmail.com>", 250},
|
||||
{"RCPT TO: <u2@gmail.com>", 250},
|
||||
{"RCPT TO:u3@gmail.com", 250},
|
||||
{"RCPT TO: u4@gmail.com", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<user\\@internal@external.com", 250},
|
||||
{"RCPT TO:<\"first last\"@host.com", 250},
|
||||
{"RCPT TO:<user\\>name@host.com>", 250},
|
||||
{"RCPT TO:<\"user>name\"@host.com>", 250},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test out recipient limit
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<u1@gmail.com>", 250},
|
||||
{"RCPT TO:<u2@gmail.com>", 250},
|
||||
{"RCPT TO:<u3@gmail.com>", 250},
|
||||
{"RCPT TO:<u4@gmail.com>", 250},
|
||||
{"RCPT TO:<u5@gmail.com>", 250},
|
||||
{"RCPT TO:<u6@gmail.com>", 552},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test DATA
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<u1@gmail.com>", 250},
|
||||
{"DATA", 354},
|
||||
{".", 250},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test RSET
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<u1@gmail.com>", 250},
|
||||
{"RSET", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test QUIT
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<u1@gmail.com>", 250},
|
||||
{"QUIT", 221},
|
||||
}
|
||||
if err := playSession(t, server, script); err != nil {
|
||||
t.Error(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 commands in DATA state
|
||||
func TestDataState(t *testing.T) {
|
||||
// Setup mock objects
|
||||
mds := &MockDataStore{}
|
||||
mb1 := &MockMailbox{}
|
||||
msg1 := &MockMessage{}
|
||||
mds.On("MailboxFor").Return(mb1, nil)
|
||||
mb1.On("NewMessage").Return(msg1)
|
||||
msg1.On("Close").Return(nil)
|
||||
|
||||
server, logbuf := setupSmtpServer(mds)
|
||||
defer teardownSmtpServer(server)
|
||||
|
||||
var script []scriptStep
|
||||
pipe := setupSmtpSession(server)
|
||||
c := textproto.NewConn(pipe)
|
||||
|
||||
// Get us into DATA state
|
||||
if code, _, err := c.ReadCodeLine(220); err != nil {
|
||||
t.Errorf("Expected a 220 greeting, got %v", code)
|
||||
}
|
||||
script = []scriptStep{
|
||||
{"HELO localhost", 250},
|
||||
{"MAIL FROM:<john@gmail.com>", 250},
|
||||
{"RCPT TO:<u1@gmail.com>", 250},
|
||||
{"DATA", 354},
|
||||
}
|
||||
if err := playScriptAgainst(t, c, script); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// Send a message
|
||||
body := `To: u1@gmail.com
|
||||
From: john@gmail.com
|
||||
Subject: test
|
||||
|
||||
Hi!
|
||||
`
|
||||
dw := c.DotWriter()
|
||||
io.WriteString(dw, body)
|
||||
dw.Close()
|
||||
if code, _, err := c.ReadCodeLine(250); err != nil {
|
||||
t.Errorf("Expected a 250 greeting, got %v", 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)
|
||||
}
|
||||
}
|
||||
|
||||
// playSession creates a new session, reads the greeting and then plays the script
|
||||
func playSession(t *testing.T, server *Server, script []scriptStep) error {
|
||||
pipe := setupSmtpSession(server)
|
||||
c := textproto.NewConn(pipe)
|
||||
|
||||
if code, _, err := c.ReadCodeLine(220); err != nil {
|
||||
return fmt.Errorf("Expected a 220 greeting, got %v", code)
|
||||
}
|
||||
|
||||
err := playScriptAgainst(t, c, script)
|
||||
|
||||
c.Cmd("QUIT")
|
||||
c.ReadCodeLine(221)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// playScriptAgainst an existing connection, does not handle server greeting
|
||||
func playScriptAgainst(t *testing.T, c *textproto.Conn, script []scriptStep) error {
|
||||
for i, step := range script {
|
||||
id, err := c.Cmd(step.send)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Step %d, failed to send %q: %v", i, step.send, err)
|
||||
}
|
||||
|
||||
c.StartResponse(id)
|
||||
code, msg, err := c.ReadCodeLine(step.expect)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Step %d, sent %q, expected %v, got %v: %q",
|
||||
i, step.send, step.expect, code, msg)
|
||||
}
|
||||
c.EndResponse(id)
|
||||
|
||||
if err != nil {
|
||||
// Return after c.EndResponse so we don't hang the connection
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// net.Pipe does not implement deadlines
|
||||
type mockConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (m *mockConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func setupSmtpServer(ds DataStore) (*Server, *bytes.Buffer) {
|
||||
// Test Server Config
|
||||
cfg := config.SmtpConfig{
|
||||
Ip4address: net.IPv4(127, 0, 0, 1),
|
||||
Ip4port: 2500,
|
||||
Domain: "inbucket.local",
|
||||
DomainNoStore: "bitbucket.local",
|
||||
MaxRecipients: 5,
|
||||
MaxIdleSeconds: 5,
|
||||
MaxMessageBytes: 5000,
|
||||
StoreMessages: true,
|
||||
}
|
||||
|
||||
// Capture log output
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
|
||||
// Create a server, don't start it
|
||||
return NewSmtpServer(cfg, ds), buf
|
||||
}
|
||||
|
||||
var sessionNum int
|
||||
|
||||
func setupSmtpSession(server *Server) net.Conn {
|
||||
// Pair of pipes to communicate
|
||||
serverConn, clientConn := net.Pipe()
|
||||
// Start the session
|
||||
server.waitgroup.Add(1)
|
||||
sessionNum++
|
||||
go server.startSession(sessionNum, &mockConn{serverConn})
|
||||
|
||||
return clientConn
|
||||
}
|
||||
|
||||
func teardownSmtpServer(server *Server) {
|
||||
//log.SetOutput(os.Stderr)
|
||||
}
|
||||
@@ -46,9 +46,7 @@ var expErrorsHist = new(expvar.String)
|
||||
var expWarnsHist = new(expvar.String)
|
||||
|
||||
// Init a new Server object
|
||||
func New() *Server {
|
||||
ds := DefaultFileDataStore()
|
||||
cfg := config.GetSmtpConfig()
|
||||
func NewSmtpServer(cfg config.SmtpConfig, ds DataStore) *Server {
|
||||
return &Server{dataStore: ds, domain: cfg.Domain, maxRecips: cfg.MaxRecipients,
|
||||
maxIdleSeconds: cfg.MaxIdleSeconds, maxMessageBytes: cfg.MaxMessageBytes,
|
||||
storeMessages: cfg.StoreMessages, domainNoStore: strings.ToLower(cfg.DomainNoStore),
|
||||
@@ -141,6 +139,7 @@ func metricsTicker(t *time.Ticker) {
|
||||
expErrorsHist.Set(pushMetric(errorsHist, expErrorsTotal))
|
||||
expWarnsHist.Set(pushMetric(warnsHist, expWarnsTotal))
|
||||
expRetentionDeletesHist.Set(pushMetric(retentionDeletesHist, expRetentionDeletesTotal))
|
||||
expRetainedHist.Set(pushMetric(retainedHist, expRetainedCurrent))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,15 @@ var retentionScanCompletedMu sync.RWMutex
|
||||
|
||||
var expRetentionDeletesTotal = new(expvar.Int)
|
||||
var expRetentionPeriod = new(expvar.Int)
|
||||
var expRetainedCurrent = new(expvar.Int)
|
||||
|
||||
// History of certain stats
|
||||
var retentionDeletesHist = list.New()
|
||||
var retainedHist = list.New()
|
||||
|
||||
// History rendered as comma delim string
|
||||
var expRetentionDeletesHist = new(expvar.String)
|
||||
var expRetainedHist = new(expvar.String)
|
||||
|
||||
func StartRetentionScanner(ds DataStore) {
|
||||
cfg := config.GetDataStoreConfig()
|
||||
@@ -62,6 +65,7 @@ func doRetentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) er
|
||||
return err
|
||||
}
|
||||
|
||||
retained := 0
|
||||
for _, mb := range mboxes {
|
||||
messages, err := mb.GetMessages()
|
||||
if err != nil {
|
||||
@@ -77,6 +81,8 @@ func doRetentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) er
|
||||
} else {
|
||||
expRetentionDeletesTotal.Add(1)
|
||||
}
|
||||
} else {
|
||||
retained++
|
||||
}
|
||||
}
|
||||
// Sleep after completing a mailbox
|
||||
@@ -84,6 +90,7 @@ func doRetentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) er
|
||||
}
|
||||
|
||||
setRetentionScanCompleted(time.Now())
|
||||
expRetainedCurrent.Set(int64(retained))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -112,4 +119,6 @@ func init() {
|
||||
rm.Set("DeletesHist", expRetentionDeletesHist)
|
||||
rm.Set("DeletesTotal", expRetentionDeletesTotal)
|
||||
rm.Set("Period", expRetentionPeriod)
|
||||
rm.Set("RetainedHist", expRetainedHist)
|
||||
rm.Set("RetainedCurrent", expRetainedCurrent)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ type MockDataStore struct {
|
||||
}
|
||||
|
||||
func (m *MockDataStore) MailboxFor(name string) (Mailbox, error) {
|
||||
return nil, nil
|
||||
args := m.Called()
|
||||
return args.Get(0).(Mailbox), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockDataStore) AllMailboxes() ([]Mailbox, error) {
|
||||
@@ -158,8 +159,8 @@ func (m *MockMessage) Size() int64 {
|
||||
}
|
||||
|
||||
func (m *MockMessage) Append(data []byte) error {
|
||||
args := m.Called(data)
|
||||
return args.Error(0)
|
||||
// []byte arg seems to mess up testify/mock
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMessage) Close() error {
|
||||
|
||||
186
smtpd/utils.go
186
smtpd/utils.go
@@ -1,6 +1,7 @@
|
||||
package smtpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
@@ -8,16 +9,36 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Take "user+ext@host.com" and return "user", aka the mailbox we'll store it in
|
||||
func ParseMailboxName(emailAddress string) (result string) {
|
||||
result = strings.ToLower(emailAddress)
|
||||
if idx := strings.Index(result, "@"); idx > -1 {
|
||||
result = result[0:idx]
|
||||
// Take "user+ext" and return "user", aka the mailbox we'll store it in
|
||||
// Return error if it contains invalid characters, we don't accept anything
|
||||
// that must be quoted according to RFC3696.
|
||||
func ParseMailboxName(localPart string) (result string, err error) {
|
||||
if localPart == "" {
|
||||
return "", fmt.Errorf("Mailbox name cannot be empty")
|
||||
}
|
||||
result = strings.ToLower(localPart)
|
||||
|
||||
invalid := make([]byte, 0, 10)
|
||||
|
||||
for i := 0; i<len(result); i++ {
|
||||
c := result[i]
|
||||
switch {
|
||||
case 'a' <= c && c <= 'z':
|
||||
case '0' <= c && c <= '9':
|
||||
case bytes.IndexByte([]byte("!#$%&'*+-=/?^_`.{|}~"), c) >= 0:
|
||||
default:
|
||||
invalid = append(invalid, c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalid) > 0 {
|
||||
return "", fmt.Errorf("Mailbox name contained invalid character(s): %q", invalid)
|
||||
}
|
||||
|
||||
if idx := strings.Index(result, "+"); idx > -1 {
|
||||
result = result[0:idx]
|
||||
}
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Take a mailbox name and hash it into the directory we'll store it in
|
||||
@@ -38,3 +59,156 @@ func JoinStringList(listOfStrings *list.List) string {
|
||||
}
|
||||
return strings.Join(s, ",")
|
||||
}
|
||||
|
||||
// ValidateDomainPart returns true if the domain part complies to RFC3696, RFC1035
|
||||
func ValidateDomainPart(domain string) bool {
|
||||
if len(domain) == 0 {
|
||||
return false
|
||||
}
|
||||
if len(domain) > 255 {
|
||||
return false
|
||||
}
|
||||
if domain[len(domain)-1] != '.' {
|
||||
domain += "."
|
||||
}
|
||||
prev := '.'
|
||||
labelLen := 0
|
||||
hasLetters := false
|
||||
|
||||
for _, c := range domain {
|
||||
switch {
|
||||
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '_':
|
||||
// Must contain some of these to be a valid label
|
||||
hasLetters = true
|
||||
labelLen++
|
||||
case '0' <= c && c <= '9':
|
||||
labelLen++
|
||||
case c == '-':
|
||||
if prev == '.' {
|
||||
// Cannot lead with hyphen
|
||||
return false
|
||||
}
|
||||
case c == '.':
|
||||
if prev == '.' || prev == '-' {
|
||||
// Cannot end with hyphen or double-dot
|
||||
return false
|
||||
}
|
||||
if labelLen > 63 {
|
||||
return false
|
||||
}
|
||||
if !hasLetters {
|
||||
return false
|
||||
}
|
||||
labelLen = 0
|
||||
hasLetters = false
|
||||
default:
|
||||
// Unknown character
|
||||
return false
|
||||
}
|
||||
prev = c
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ParseEmailAddress unescapes an email address, and splits the local part from the domain part.
|
||||
// An error is returned if the local or domain parts fail validation following the guidelines
|
||||
// in RFC3696.
|
||||
func ParseEmailAddress(address string) (local string, domain string, err error) {
|
||||
if address == "" {
|
||||
return "", "", fmt.Errorf("Empty address")
|
||||
}
|
||||
if len(address) > 320 {
|
||||
return "", "", fmt.Errorf("Address exceeds 320 characters")
|
||||
}
|
||||
if address[0] == '@' {
|
||||
return "", "", fmt.Errorf("Address cannot start with @ symbol")
|
||||
}
|
||||
if address[0] == '.' {
|
||||
return "", "", fmt.Errorf("Address cannot start with a period")
|
||||
}
|
||||
|
||||
// Loop over address parsing out local part
|
||||
buf := new(bytes.Buffer)
|
||||
prev := byte('.')
|
||||
inCharQuote := false
|
||||
inStringQuote := false
|
||||
LOOP:
|
||||
for i := 0; i < len(address); i++ {
|
||||
c := address[i]
|
||||
switch {
|
||||
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'):
|
||||
// Letters are OK
|
||||
buf.WriteByte(c)
|
||||
inCharQuote = false
|
||||
case '0' <= c && c <= '9':
|
||||
// Numbers are OK
|
||||
buf.WriteByte(c)
|
||||
inCharQuote = false
|
||||
case bytes.IndexByte([]byte("!#$%&'*+-/=?^_`{|}~"), c) >= 0:
|
||||
// These specials can be used unquoted
|
||||
buf.WriteByte(c)
|
||||
inCharQuote = false
|
||||
case c == '.':
|
||||
// A single period is OK
|
||||
if prev == '.' {
|
||||
// Sequence of periods is not permitted
|
||||
return "", "", fmt.Errorf("Sequence of periods is not permitted")
|
||||
}
|
||||
buf.WriteByte(c)
|
||||
inCharQuote = false
|
||||
case c == '\\':
|
||||
inCharQuote = true
|
||||
case c == '"':
|
||||
if inCharQuote {
|
||||
buf.WriteByte(c)
|
||||
inCharQuote = false
|
||||
} else if inStringQuote {
|
||||
inStringQuote = false
|
||||
} else {
|
||||
if i == 0 {
|
||||
inStringQuote = true
|
||||
} else {
|
||||
return "", "", fmt.Errorf("Quoted string can only begin at start of address")
|
||||
}
|
||||
}
|
||||
case c == '@':
|
||||
if inCharQuote || inStringQuote {
|
||||
buf.WriteByte(c)
|
||||
inCharQuote = false
|
||||
} else {
|
||||
// End of local-part
|
||||
if i > 63 {
|
||||
return "", "", fmt.Errorf("Local part must not exceed 64 characters")
|
||||
}
|
||||
if prev == '.' {
|
||||
return "", "", fmt.Errorf("Local part cannot end with a period")
|
||||
}
|
||||
domain = address[i+1:]
|
||||
break LOOP
|
||||
}
|
||||
case c > 127:
|
||||
return "", "", fmt.Errorf("Characters outside of US-ASCII range not permitted")
|
||||
default:
|
||||
if inCharQuote || inStringQuote {
|
||||
buf.WriteByte(c)
|
||||
inCharQuote = false
|
||||
} else {
|
||||
return "", "", fmt.Errorf("Character %q must be quoted", c)
|
||||
}
|
||||
}
|
||||
prev = c
|
||||
}
|
||||
if inCharQuote {
|
||||
return "", "", fmt.Errorf("Cannot end address with unterminated quoted-pair")
|
||||
}
|
||||
if inStringQuote {
|
||||
return "", "", fmt.Errorf("Cannot end address with unterminated string quote")
|
||||
}
|
||||
|
||||
if !ValidateDomainPart(domain) {
|
||||
return "", "", fmt.Errorf("Domain part validation failed")
|
||||
}
|
||||
|
||||
return buf.String(), domain, nil
|
||||
}
|
||||
|
||||
@@ -2,15 +2,211 @@ package smtpd
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseMailboxName(t *testing.T) {
|
||||
assert.Equal(t, ParseMailboxName("MailBOX"), "mailbox")
|
||||
assert.Equal(t, ParseMailboxName("MailBox@Host.Com"), "mailbox")
|
||||
assert.Equal(t, ParseMailboxName("Mail+extra@Host.Com"), "mail")
|
||||
var validTable = []struct{
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{"mailbox", "mailbox"},
|
||||
{"user123", "user123"},
|
||||
{"MailBOX", "mailbox"},
|
||||
{"First.Last", "first.last"},
|
||||
{"user+label", "user"},
|
||||
{"chars!#$%", "chars!#$%"},
|
||||
{"chars&'*-", "chars&'*-"},
|
||||
{"chars=/?^", "chars=/?^"},
|
||||
{"chars_`.{", "chars_`.{"},
|
||||
{"chars|}~", "chars|}~"},
|
||||
}
|
||||
|
||||
for _, tt := range validTable {
|
||||
if result, err := ParseMailboxName(tt.input); err != nil {
|
||||
t.Errorf("Error while parsing %q: %v", tt.input, err)
|
||||
} else {
|
||||
if result != tt.expect {
|
||||
t.Errorf("Parsing %q, expected %q, got %q", tt.input, tt.expect, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invalidTable = []struct{
|
||||
input, msg string
|
||||
}{
|
||||
{"", "Empty mailbox name is not permitted"},
|
||||
{"user@host", "@ symbol not permitted"},
|
||||
{"first last", "Space not permitted"},
|
||||
{"first\"last", "Double quote not permitted"},
|
||||
{"first\nlast", "Control chars not permitted"},
|
||||
}
|
||||
|
||||
for _, tt := range invalidTable {
|
||||
if _, err := ParseMailboxName(tt.input); err == nil {
|
||||
t.Errorf("Didn't get an error while parsing %q: %v", tt.input, tt.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashMailboxName(t *testing.T) {
|
||||
assert.Equal(t, HashMailboxName("mail"), "1d6e1cf70ec6f9ab28d3ea4b27a49a77654d370e")
|
||||
}
|
||||
|
||||
func TestValidateDomain(t *testing.T) {
|
||||
assert.False(t, ValidateDomainPart(strings.Repeat("a", 256)),
|
||||
"Max domain length is 255")
|
||||
assert.False(t, ValidateDomainPart(strings.Repeat("a", 64)+".com"),
|
||||
"Max label length is 63")
|
||||
assert.True(t, ValidateDomainPart(strings.Repeat("a", 63)+".com"),
|
||||
"Should allow 63 char label")
|
||||
|
||||
var testTable = []struct {
|
||||
input string
|
||||
expect bool
|
||||
msg string
|
||||
}{
|
||||
{"", false, "Empty domain is not valid"},
|
||||
{"hostname", true, "Just a hostname is valid"},
|
||||
{"github.com", true, "Two labels should be just fine"},
|
||||
{"my-domain.com", true, "Hyphen is allowed mid-label"},
|
||||
{"_domainkey.foo.com", true, "Underscores are allowed"},
|
||||
{"bar.com.", true, "Must be able to end with a dot"},
|
||||
{"ABC.6DBS.com", true, "Mixed case is OK"},
|
||||
{"google..com", false, "Double dot not valid"},
|
||||
{".foo.com", false, "Cannot start with a dot"},
|
||||
{"mail.123.com", false, "Number only label not valid"},
|
||||
{"google\r.com", false, "Special chars not allowed"},
|
||||
{"foo.-bar.com", false, "Label cannot start with hyphen"},
|
||||
{"foo-.bar.com", false, "Label cannot end with hyphen"},
|
||||
}
|
||||
|
||||
for _, tt := range testTable {
|
||||
if ValidateDomainPart(tt.input) != tt.expect {
|
||||
t.Errorf("Expected %v for %q: %s", tt.expect, tt.input, tt.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLocal(t *testing.T) {
|
||||
var testTable = []struct {
|
||||
input string
|
||||
expect bool
|
||||
msg string
|
||||
}{
|
||||
{"", false, "Empty local is not valid"},
|
||||
{"a", true, "Single letter should be fine"},
|
||||
{strings.Repeat("a", 65), false, "Only valid up to 64 characters"},
|
||||
{"FirstLast", true, "Mixed case permitted"},
|
||||
{"user123", true, "Numbers permitted"},
|
||||
{"a!#$%&'*+-/=?^_`{|}~", true, "Any of !#$%&'*+-/=?^_`{|}~ are permitted"},
|
||||
{"first.last", true, "Embedded period is permitted"},
|
||||
{"first..last", false, "Sequence of periods is not allowed"},
|
||||
{".user", false, "Cannot lead with a period"},
|
||||
{"user.", false, "Cannot end with a period"},
|
||||
{"james@mail", false, "Unquoted @ not permitted"},
|
||||
{"first last", false, "Unquoted space not permitted"},
|
||||
{"tricky\\. ", false, "Unquoted space not permitted"},
|
||||
{"no,commas", false, "Unquoted comma not allowed"},
|
||||
{"t[es]t", false, "Unquoted square brackets not allowed"},
|
||||
{"james\\", false, "Cannot end with backslash quote"},
|
||||
{"james\\@mail", true, "Quoted @ permitted"},
|
||||
{"quoted\\ space", true, "Quoted space permitted"},
|
||||
{"no\\,commas", true, "Quoted comma is OK"},
|
||||
{"t\\[es\\]t", true, "Quoted brackets are OK"},
|
||||
{"user\\name", true, "Should be able to quote a-z"},
|
||||
{"USER\\NAME", true, "Should be able to quote A-Z"},
|
||||
{"user\\1", true, "Should be able to quote a digit"},
|
||||
{"one\\$\\|", true, "Should be able to quote plain specials"},
|
||||
{"return\\\r", true, "Should be able to quote ASCII control chars"},
|
||||
{"high\\\x80", false, "Should not accept > 7-bit quoted chars"},
|
||||
{"quote\\\"", true, "Quoted double quote is permitted"},
|
||||
{"\"james\"", true, "Quoted a-z is permitted"},
|
||||
{"\"first last\"", true, "Quoted space is permitted"},
|
||||
{"\"quoted@sign\"", true, "Quoted @ is allowed"},
|
||||
{"\"qp\\\"quote\"", true, "Quoted quote within quoted string is OK"},
|
||||
{"\"unterminated", false, "Quoted string must be terminated"},
|
||||
{"\"unterminated\\\"", false, "Quoted string must be terminated"},
|
||||
{"embed\"quote\"string", false, "Embedded quoted string is illegal"},
|
||||
|
||||
{"user+mailbox", true, "RFC3696 test case should be valid"},
|
||||
{"customer/department=shipping", true, "RFC3696 test case should be valid"},
|
||||
{"$A12345", true, "RFC3696 test case should be valid"},
|
||||
{"!def!xyz%abc", true, "RFC3696 test case should be valid"},
|
||||
{"_somename", true, "RFC3696 test case should be valid"},
|
||||
}
|
||||
|
||||
for _, tt := range testTable {
|
||||
_, _, err := ParseEmailAddress(tt.input + "@domain.com")
|
||||
if (err != nil) == tt.expect {
|
||||
if err != nil {
|
||||
t.Logf("Got error: %s", err)
|
||||
}
|
||||
t.Errorf("Expected %v for %q: %s", tt.expect, tt.input, tt.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEmailAddress(t *testing.T) {
|
||||
// Test some good email addresses
|
||||
var testTable = []struct {
|
||||
input, local, domain string
|
||||
}{
|
||||
{"root@localhost", "root", "localhost"},
|
||||
{"FirstLast@domain.local", "FirstLast", "domain.local"},
|
||||
{"route66@prodigy.net", "route66", "prodigy.net"},
|
||||
{"lorbit!user@uucp", "lorbit!user", "uucp"},
|
||||
{"user+spam@gmail.com", "user+spam", "gmail.com"},
|
||||
{"first.last@domain.local", "first.last", "domain.local"},
|
||||
{"first\\ last@_key.domain.com", "first last", "_key.domain.com"},
|
||||
{"first\\\"last@a.b.c", "first\"last", "a.b.c"},
|
||||
{"user\\@internal@myhost.ca", "user@internal", "myhost.ca"},
|
||||
{"\"first last@evil\"@top-secret.gov", "first last@evil", "top-secret.gov"},
|
||||
{"\"line\nfeed\"@linenoise.co.uk", "line\nfeed", "linenoise.co.uk"},
|
||||
{"user+mailbox@host", "user+mailbox", "host"},
|
||||
{"customer/department=shipping@host", "customer/department=shipping", "host"},
|
||||
{"$A12345@host", "$A12345", "host"},
|
||||
{"!def!xyz%abc@host", "!def!xyz%abc", "host"},
|
||||
{"_somename@host", "_somename", "host"},
|
||||
}
|
||||
|
||||
for _, tt := range testTable {
|
||||
local, domain, err := ParseEmailAddress(tt.input)
|
||||
if err != nil {
|
||||
t.Errorf("Error when parsing %q: %s", tt.input, err)
|
||||
} else {
|
||||
if tt.local != local {
|
||||
t.Errorf("When parsing %q, expected local %q, got %q instead",
|
||||
tt.input, tt.local, local)
|
||||
}
|
||||
if tt.domain != domain {
|
||||
t.Errorf("When parsing %q, expected domain %q, got %q instead",
|
||||
tt.input, tt.domain, domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that validations fail correctly
|
||||
var badTable = []struct {
|
||||
input, msg string
|
||||
}{
|
||||
{"", "Empty address not permitted"},
|
||||
{"user", "Missing domain part"},
|
||||
{"@host", "Missing local part"},
|
||||
{"user\\@host", "Missing domain part"},
|
||||
{"\"user@host\"", "Missing domain part"},
|
||||
{"\"user@host", "Unterminated quoted string"},
|
||||
{"first last@host", "Unquoted space"},
|
||||
{"user@bad!domain", "Invalid domain"},
|
||||
{".user@host", "Can't lead with a ."},
|
||||
{"user.@host", "Can't end local with a dot"},
|
||||
{"user@bad domain", "No spaces in domain permitted"},
|
||||
}
|
||||
|
||||
for _, tt := range badTable {
|
||||
if _, _, err := ParseEmailAddress(tt.input); err == nil {
|
||||
t.Errorf("Did not get expected error when parsing %q: %s", tt.input, tt.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
{{$name := .name}}
|
||||
{{$id := .message.Id}}
|
||||
<div id="emailActions">
|
||||
<a href="javascript:deleteMessage('{{.message.Id}}');">Delete</a>
|
||||
<a href="javascript:messageSource('{{.message.Id}}');">Source</a>
|
||||
{{if .htmlAvailable}}
|
||||
<a href="javascript:htmlView('{{.message.Id}}');">HTML</a>
|
||||
{{end}}
|
||||
<a href="/link/{{$name}}/{{$id}}">Link</a>
|
||||
<a href="javascript:deleteMessage('{{.message.Id}}');">Delete</a>
|
||||
<a href="javascript:messageSource('{{.message.Id}}');">Source</a>
|
||||
{{if .htmlAvailable}}
|
||||
<a href="javascript:htmlView('{{.message.Id}}');">HTML</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<table id="emailHeader">
|
||||
<tr>
|
||||
<th>From:</th>
|
||||
<td>{{.message.From}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date:</th>
|
||||
<td>{{.message.Date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>From:</th>
|
||||
<td>{{.message.From}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date:</th>
|
||||
<td>{{.message.Date}}</td>
|
||||
</tr>
|
||||
<table>
|
||||
|
||||
{{with .attachments}}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
{{define "script"}}
|
||||
<script>
|
||||
function listLoaded() {
|
||||
var selected = "{{.selected}}"
|
||||
function listLoaded() {
|
||||
$('.listEntry').hover(
|
||||
function() {
|
||||
$(this).addClass("listEntryHover")
|
||||
@@ -19,6 +20,10 @@
|
||||
}
|
||||
)
|
||||
$("#messageList").slideDown()
|
||||
if (selected != "") {
|
||||
$("#" + selected).click()
|
||||
selected = ""
|
||||
}
|
||||
}
|
||||
|
||||
function loadList() {
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setHistory(name, value) {
|
||||
// Show spikes for numbers that only increase
|
||||
function setHistoryOfActivity(name, value) {
|
||||
var h = value.split(",")
|
||||
var prev = parseInt(h[0])
|
||||
for (i=0; i<h.length; i++) {
|
||||
@@ -71,6 +72,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Show up/down for numbers that can decrease
|
||||
function setHistoryOfCount(name, value) {
|
||||
var h = value.split(",")
|
||||
el = $('#s-' + name)
|
||||
if (el) {
|
||||
el.sparkline(h)
|
||||
}
|
||||
}
|
||||
|
||||
function metric(name, value, filter, chartable) {
|
||||
if (chartable) {
|
||||
appendHistory(name, value)
|
||||
@@ -101,15 +111,17 @@
|
||||
|
||||
// Server-side history
|
||||
metric('smtpReceivedTotal', data.smtp.ReceivedTotal, numberFilter, false)
|
||||
setHistory('smtpReceivedTotal', data.smtp.ReceivedHist)
|
||||
setHistoryOfActivity('smtpReceivedTotal', data.smtp.ReceivedHist)
|
||||
metric('smtpConnectsTotal', data.smtp.ConnectsTotal, numberFilter, false)
|
||||
setHistory('smtpConnectsTotal', data.smtp.ConnectsHist)
|
||||
setHistoryOfActivity('smtpConnectsTotal', data.smtp.ConnectsHist)
|
||||
metric('smtpWarnsTotal', data.smtp.WarnsTotal, numberFilter, false)
|
||||
setHistory('smtpWarnsTotal', data.smtp.WarnsHist)
|
||||
setHistoryOfActivity('smtpWarnsTotal', data.smtp.WarnsHist)
|
||||
metric('smtpErrorsTotal', data.smtp.ErrorsTotal, numberFilter, false)
|
||||
setHistory('smtpErrorsTotal', data.smtp.ErrorsHist)
|
||||
setHistoryOfActivity('smtpErrorsTotal', data.smtp.ErrorsHist)
|
||||
metric('retentionDeletesTotal', data.retention.DeletesTotal, numberFilter, false)
|
||||
setHistory('retentionDeletesTotal', data.retention.DeletesHist)
|
||||
setHistoryOfActivity('retentionDeletesTotal', data.retention.DeletesHist)
|
||||
metric('retainedCurrent', data.retention.RetainedCurrent, numberFilter, false)
|
||||
setHistoryOfCount('retainedCurrent', data.retention.RetainedHist)
|
||||
}
|
||||
|
||||
function loadMetrics() {
|
||||
@@ -233,21 +245,21 @@
|
||||
<tr>
|
||||
<th>Retention Period:</th>
|
||||
<td colspan="3">
|
||||
{{if .retentionMinutes}}
|
||||
<span id="m-retentionPeriod">.</span>
|
||||
{{else}}
|
||||
Disabled
|
||||
{{end}}
|
||||
{{if .retentionMinutes}}
|
||||
<span id="m-retentionPeriod">.</span>
|
||||
{{else}}
|
||||
Disabled
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Retention Scan:</th>
|
||||
<td colspan="3">
|
||||
{{if .retentionMinutes}}
|
||||
Completed <span id="m-retentionScanCompleted">.</span> ago
|
||||
{{else}}
|
||||
Disabled
|
||||
{{end}}
|
||||
{{if .retentionMinutes}}
|
||||
Completed <span id="m-retentionScanCompleted">.</span> ago
|
||||
{{else}}
|
||||
Disabled
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -256,6 +268,12 @@
|
||||
<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="sparkline"><span id="s-retainedCurrent"></span></td>
|
||||
<td>(60min)</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p class="last"> </p>
|
||||
</div>
|
||||
|
||||
@@ -39,11 +39,10 @@ func headerMatch(req *http.Request, name string, value string) bool {
|
||||
func NewContext(req *http.Request) (*Context, error) {
|
||||
vars := mux.Vars(req)
|
||||
sess, err := sessionStore.Get(req, "inbucket")
|
||||
ds := smtpd.DefaultFileDataStore()
|
||||
ctx := &Context{
|
||||
Vars: vars,
|
||||
Session: sess,
|
||||
DataStore: ds,
|
||||
DataStore: DataStore,
|
||||
IsJson: headerMatch(req, "Accept", "application/json"),
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package web
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jhillyerd/inbucket/log"
|
||||
"github.com/jhillyerd/inbucket/smtpd"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -30,23 +31,51 @@ type JsonMessageBody struct {
|
||||
}
|
||||
|
||||
func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Form values must be validated manually
|
||||
name := req.FormValue("name")
|
||||
selected := req.FormValue("id")
|
||||
|
||||
if len(name) == 0 {
|
||||
ctx.Session.AddFlash("Account name is required", "errors")
|
||||
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
||||
name, err = smtpd.ParseMailboxName(name)
|
||||
if err != nil {
|
||||
ctx.Session.AddFlash(err.Error(), "errors")
|
||||
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
||||
return RenderTemplate("mailbox/index.html", w, map[string]interface{}{
|
||||
"ctx": ctx,
|
||||
"name": name,
|
||||
"ctx": ctx,
|
||||
"name": name,
|
||||
"selected": selected,
|
||||
})
|
||||
}
|
||||
|
||||
func MailboxLink(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||
id := ctx.Vars["id"]
|
||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||
if err != nil {
|
||||
ctx.Session.AddFlash(err.Error(), "errors")
|
||||
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?name=%s&id=%s", reverse("MailboxIndex"), name, id)
|
||||
http.Redirect(w, req, uri, http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
||||
func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||
name := ctx.Vars["name"]
|
||||
|
||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb, err := ctx.DataStore.MailboxFor(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get mailbox for %v: %v", name, err)
|
||||
@@ -81,9 +110,11 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
||||
|
||||
func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||
name := ctx.Vars["name"]
|
||||
id := ctx.Vars["id"]
|
||||
|
||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb, err := ctx.DataStore.MailboxFor(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MailboxFor('%v'): %v", name, err)
|
||||
@@ -133,8 +164,10 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
||||
|
||||
func MailboxPurge(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||
name := ctx.Vars["name"]
|
||||
|
||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb, err := ctx.DataStore.MailboxFor(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MailboxFor('%v'): %v", name, err)
|
||||
@@ -155,9 +188,11 @@ func MailboxPurge(w http.ResponseWriter, req *http.Request, ctx *Context) (err e
|
||||
|
||||
func MailboxHtml(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||
name := ctx.Vars["name"]
|
||||
id := ctx.Vars["id"]
|
||||
|
||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb, err := ctx.DataStore.MailboxFor(name)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -182,9 +217,11 @@ func MailboxHtml(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
|
||||
|
||||
func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||
name := ctx.Vars["name"]
|
||||
id := ctx.Vars["id"]
|
||||
|
||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb, err := ctx.DataStore.MailboxFor(name)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -205,8 +242,13 @@ func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err
|
||||
|
||||
func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||
name := ctx.Vars["name"]
|
||||
id := ctx.Vars["id"]
|
||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||
if err != nil {
|
||||
ctx.Session.AddFlash(err.Error(), "errors")
|
||||
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
numStr := ctx.Vars["num"]
|
||||
num, err := strconv.ParseUint(numStr, 10, 32)
|
||||
if err != nil {
|
||||
@@ -242,7 +284,12 @@ func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *Contex
|
||||
|
||||
func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||
name := ctx.Vars["name"]
|
||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||
if err != nil {
|
||||
ctx.Session.AddFlash(err.Error(), "errors")
|
||||
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
id := ctx.Vars["id"]
|
||||
numStr := ctx.Vars["num"]
|
||||
num, err := strconv.ParseUint(numStr, 10, 32)
|
||||
@@ -278,9 +325,11 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (
|
||||
|
||||
func MailboxDelete(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
|
||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||
name := ctx.Vars["name"]
|
||||
id := ctx.Vars["id"]
|
||||
|
||||
name, err := smtpd.ParseMailboxName(ctx.Vars["name"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb, err := ctx.DataStore.MailboxFor(name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jhillyerd/inbucket/config"
|
||||
"github.com/jhillyerd/inbucket/log"
|
||||
"github.com/jhillyerd/inbucket/smtpd"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
|
||||
type handler func(http.ResponseWriter, *http.Request, *Context) error
|
||||
|
||||
var DataStore smtpd.DataStore
|
||||
var Router *mux.Router
|
||||
var listener net.Listener
|
||||
var sessionStore sessions.Store
|
||||
@@ -34,6 +36,7 @@ func setupRoutes(cfg config.WebConfig) {
|
||||
// Root
|
||||
r.Path("/").Handler(handler(RootIndex)).Name("RootIndex").Methods("GET")
|
||||
r.Path("/status").Handler(handler(RootStatus)).Name("RootStatus").Methods("GET")
|
||||
r.Path("/link/{name}/{id}").Handler(handler(MailboxLink)).Name("MailboxLink").Methods("GET")
|
||||
r.Path("/mailbox").Handler(handler(MailboxIndex)).Name("MailboxIndex").Methods("GET")
|
||||
r.Path("/mailbox/{name}").Handler(handler(MailboxList)).Name("MailboxList").Methods("GET")
|
||||
r.Path("/mailbox/{name}").Handler(handler(MailboxPurge)).Name("MailboxPurge").Methods("DELETE")
|
||||
@@ -54,6 +57,9 @@ 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"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user