1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-18 10:07:02 +00:00

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
This commit is contained in:
James Hillyerd
2013-11-12 10:42:39 -08:00
parent 414ed44882
commit 46fa714cc7
12 changed files with 177 additions and 41 deletions

View File

@@ -42,6 +42,7 @@ type DataStoreConfig struct {
Path string Path string
RetentionMinutes int RetentionMinutes int
RetentionSleep int RetentionSleep int
MailboxMsgCap int
} }
var ( var (
@@ -121,6 +122,7 @@ func LoadConfig(filename string) error {
requireOption(messages, "datastore", "path") requireOption(messages, "datastore", "path")
requireOption(messages, "datastore", "retention.minutes") requireOption(messages, "datastore", "retention.minutes")
requireOption(messages, "datastore", "retention.sleep.millis") requireOption(messages, "datastore", "retention.sleep.millis")
requireOption(messages, "datastore", "mailbox.message.cap")
// Return error if validations failed // Return error if validations failed
if messages.Len() > 0 { if messages.Len() > 0 {
@@ -361,6 +363,11 @@ func parseDataStoreConfig() error {
if err != nil { if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err) return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
} }
option = "mailbox.message.cap"
dataStoreConfig.MailboxMsgCap, err = Config.Int(section, option)
if err != nil {
return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err)
}
return nil return nil
} }

View File

@@ -100,3 +100,8 @@ retention.minutes=0
# This should help reduce disk I/O when there are a large number of messages # This should help reduce disk I/O when there are a large number of messages
# to purge. # to purge.
retention.sleep.millis=100 retention.sleep.millis=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=100

View File

@@ -100,3 +100,13 @@ retention.minutes=240
# This should help reduce disk I/O when there are a large number of messages # This should help reduce disk I/O when there are a large number of messages
# to purge. # to purge.
retention.sleep.millis=100 retention.sleep.millis=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=500

View File

@@ -100,3 +100,8 @@ retention.minutes=240
# This should help reduce disk I/O when there are a large number of messages # This should help reduce disk I/O when there are a large number of messages
# to purge. # to purge.
retention.sleep.millis=100 retention.sleep.millis=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=500

View File

@@ -100,3 +100,8 @@ retention.minutes=240
# This should help reduce disk I/O when there are a large number of messages # This should help reduce disk I/O when there are a large number of messages
# to purge. # to purge.
retention.sleep.millis=100 retention.sleep.millis=100
# Maximum number of messages we will store in a single mailbox. If this
# number is exceeded, the oldest message in the box will be deleted each
# time a new message is received for it.
mailbox.message.cap=500

View File

@@ -16,7 +16,7 @@ type Mailbox interface {
GetMessages() ([]Message, error) GetMessages() ([]Message, error)
GetMessage(id string) (Message, error) GetMessage(id string) (Message, error)
Purge() error Purge() error
NewMessage() Message NewMessage() (Message, error)
String() string String() string
} }

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 // A DataStore is the root of the mail storage hiearchy. It provides access to
// Mailbox objects // Mailbox objects
type FileDataStore struct { type FileDataStore struct {
path string path string
mailPath string mailPath string
messageCap int
} }
// NewFileDataStore creates a new DataStore object using the specified path // NewFileDataStore creates a new DataStore object using the specified path
func NewFileDataStore(path string) DataStore { func NewFileDataStore(cfg config.DataStoreConfig) DataStore {
path := cfg.Path
if path == "" {
log.LogError("No value configured for datastore path")
return nil
}
mailPath := filepath.Join(path, "mail") mailPath := filepath.Join(path, "mail")
if _, err := os.Stat(mailPath); err != nil { if _, err := os.Stat(mailPath); err != nil {
// Mail datastore does not yet exist // Mail datastore does not yet exist
os.MkdirAll(mailPath, 0770) os.MkdirAll(mailPath, 0770)
} }
return &FileDataStore{path: path, mailPath: mailPath} return &FileDataStore{path: path, mailPath: mailPath, messageCap: cfg.MailboxMsgCap}
} }
// DefaultFileDataStore creates a new DataStore object. It uses the inbucket.Config object to // DefaultFileDataStore creates a new DataStore object. It uses the inbucket.Config object to
// construct it's path. // construct it's path.
func DefaultFileDataStore() DataStore { func DefaultFileDataStore() DataStore {
path, err := config.Config.String("datastore", "path") cfg := config.GetDataStoreConfig()
if err != nil { return NewFileDataStore(cfg)
log.LogError("Error getting datastore path: %v", err)
return nil
}
if path == "" {
log.LogError("No value configured for datastore path")
return nil
}
return NewFileDataStore(path)
} }
// Retrieves the Mailbox object for a specified email address, if the mailbox // Retrieves the Mailbox object for a specified email address, if the mailbox
@@ -292,11 +290,28 @@ type FileMessage struct {
} }
// NewMessage creates a new Message object and sets the Date and Id fields. // NewMessage creates a new Message object and sets the Date and Id fields.
func (mb *FileMailbox) NewMessage() Message { // It will also delete messages over messageCap if configured.
func (mb *FileMailbox) NewMessage() (Message, error) {
// Load index
if !mb.indexLoaded {
if err := mb.readIndex(); err != nil {
return nil, err
}
}
// Delete old messages over messageCap
if mb.store.messageCap > 0 {
for len(mb.messages) >= mb.store.messageCap {
log.LogInfo("Mailbox %q over configured message cap", mb.name)
if err := mb.messages[0].Delete(); err != nil {
return nil, err
}
}
}
date := time.Now() date := time.Now()
id := generateId(date) id := generateId(date)
return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}, nil
return &FileMessage{mailbox: mb, Fid: id, Fdate: date, writable: true}
} }
func (m *FileMessage) Id() string { func (m *FileMessage) Id() string {

View File

@@ -3,6 +3,7 @@ package smtpd
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/jhillyerd/inbucket/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"io" "io"
"io/ioutil" "io/ioutil"
@@ -15,7 +16,7 @@ import (
// Test directory structure created by filestore // Test directory structure created by filestore
func TestFSDirStructure(t *testing.T) { func TestFSDirStructure(t *testing.T) {
ds, logbuf := setupDataStore() ds, logbuf := setupDataStore(config.DataStoreConfig{})
defer teardownDataStore(ds) defer teardownDataStore(ds)
root := ds.path root := ds.path
@@ -99,7 +100,7 @@ func TestFSDirStructure(t *testing.T) {
// Test FileDataStore.AllMailboxes() // Test FileDataStore.AllMailboxes()
func TestFSAllMailboxes(t *testing.T) { func TestFSAllMailboxes(t *testing.T) {
ds, logbuf := setupDataStore() ds, logbuf := setupDataStore(config.DataStoreConfig{})
defer teardownDataStore(ds) defer teardownDataStore(ds)
for _, name := range []string{"abby", "bill", "christa", "donald", "evelyn"} { for _, name := range []string{"abby", "bill", "christa", "donald", "evelyn"} {
@@ -127,7 +128,7 @@ func TestFSAllMailboxes(t *testing.T) {
// Test delivering several messages to the same mailbox, meanwhile querying its // Test delivering several messages to the same mailbox, meanwhile querying its
// contents with a new mailbox object each time // contents with a new mailbox object each time
func TestFSDeliverMany(t *testing.T) { func TestFSDeliverMany(t *testing.T) {
ds, logbuf := setupDataStore() ds, logbuf := setupDataStore(config.DataStoreConfig{})
defer teardownDataStore(ds) defer teardownDataStore(ds)
mbName := "fred" mbName := "fred"
@@ -176,7 +177,7 @@ func TestFSDeliverMany(t *testing.T) {
// Test deleting messages // Test deleting messages
func TestFSDelete(t *testing.T) { func TestFSDelete(t *testing.T) {
ds, logbuf := setupDataStore() ds, logbuf := setupDataStore(config.DataStoreConfig{})
defer teardownDataStore(ds) defer teardownDataStore(ds)
mbName := "fred" mbName := "fred"
@@ -250,7 +251,7 @@ func TestFSDelete(t *testing.T) {
// Test purging a mailbox // Test purging a mailbox
func TestFSPurge(t *testing.T) { func TestFSPurge(t *testing.T) {
ds, logbuf := setupDataStore() ds, logbuf := setupDataStore(config.DataStoreConfig{})
defer teardownDataStore(ds) defer teardownDataStore(ds)
mbName := "fred" mbName := "fred"
@@ -298,7 +299,7 @@ func TestFSPurge(t *testing.T) {
// Test message size calculation // Test message size calculation
func TestFSSize(t *testing.T) { func TestFSSize(t *testing.T) {
ds, logbuf := setupDataStore() ds, logbuf := setupDataStore(config.DataStoreConfig{})
defer teardownDataStore(ds) defer teardownDataStore(ds)
mbName := "fred" mbName := "fred"
@@ -336,7 +337,7 @@ func TestFSSize(t *testing.T) {
// Test missing files // Test missing files
func TestFSMissing(t *testing.T) { func TestFSMissing(t *testing.T) {
ds, logbuf := setupDataStore() ds, logbuf := setupDataStore(config.DataStoreConfig{})
defer teardownDataStore(ds) defer teardownDataStore(ds)
mbName := "fred" mbName := "fred"
@@ -376,8 +377,88 @@ func TestFSMissing(t *testing.T) {
} }
} }
// Test delivering several messages to the same mailbox, see if message cap works
func TestFSMessageCap(t *testing.T) {
mbCap := 10
ds, logbuf := setupDataStore(config.DataStoreConfig{MailboxMsgCap: mbCap})
defer teardownDataStore(ds)
mbName := "captain"
for i := 0; i < 20; i++ {
// Add a message
subj := fmt.Sprintf("subject %v", i)
deliverMessage(ds, mbName, subj, time.Now())
t.Logf("Delivered %q", subj)
// Check number of messages
mb, err := ds.MailboxFor(mbName)
if err != nil {
t.Fatalf("Failed to MailboxFor(%q): %v", mbName, err)
}
msgs, err := mb.GetMessages()
if err != nil {
t.Fatalf("Failed to GetMessages for %q: %v", mbName, err)
}
if len(msgs) > mbCap {
t.Errorf("Mailbox should be capped at %v messages, but has %v", mbCap, len(msgs))
}
// Check that the first message is correct
first := i - mbCap + 1
if first < 0 {
first = 0
}
firstSubj := fmt.Sprintf("subject %v", first)
if firstSubj != msgs[0].Subject() {
t.Errorf("Expected first subject to be %q, got %q", firstSubj, msgs[0].Subject())
}
}
if t.Failed() {
// Wait for handler to finish logging
time.Sleep(2 * time.Second)
// Dump buffered log data if there was a failure
io.Copy(os.Stderr, logbuf)
}
}
// Test delivering several messages to the same mailbox, see if no message cap works
func TestFSNoMessageCap(t *testing.T) {
mbCap := 0
ds, logbuf := setupDataStore(config.DataStoreConfig{MailboxMsgCap: mbCap})
defer teardownDataStore(ds)
mbName := "captain"
for i := 0; i < 20; i++ {
// Add a message
subj := fmt.Sprintf("subject %v", i)
deliverMessage(ds, mbName, subj, time.Now())
t.Logf("Delivered %q", subj)
// Check number of messages
mb, err := ds.MailboxFor(mbName)
if err != nil {
t.Fatalf("Failed to MailboxFor(%q): %v", mbName, err)
}
msgs, err := mb.GetMessages()
if err != nil {
t.Fatalf("Failed to GetMessages for %q: %v", mbName, err)
}
if len(msgs) != i+1 {
t.Errorf("Expected %v messages, got %v", i+1, len(msgs))
}
}
if t.Failed() {
// Wait for handler to finish logging
time.Sleep(2 * time.Second)
// Dump buffered log data if there was a failure
io.Copy(os.Stderr, logbuf)
}
}
// setupDataStore creates a new FileDataStore in a temporary directory // setupDataStore creates a new FileDataStore in a temporary directory
func setupDataStore() (*FileDataStore, *bytes.Buffer) { func setupDataStore(cfg config.DataStoreConfig) (*FileDataStore, *bytes.Buffer) {
path, err := ioutil.TempDir("", "inbucket") path, err := ioutil.TempDir("", "inbucket")
if err != nil { if err != nil {
panic(err) panic(err)
@@ -387,12 +468,14 @@ func setupDataStore() (*FileDataStore, *bytes.Buffer) {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
log.SetOutput(buf) log.SetOutput(buf)
return NewFileDataStore(path).(*FileDataStore), buf cfg.Path = path
return NewFileDataStore(cfg).(*FileDataStore), buf
} }
// deliverMessage creates and delivers a message to the specific mailbox, returning // deliverMessage creates and delivers a message to the specific mailbox, returning
// the size of the generated message. // the size of the generated message.
func deliverMessage(ds *FileDataStore, mbName string, subject string, date time.Time) (id string, size int) { func deliverMessage(ds *FileDataStore, mbName string, subject string,
date time.Time) (id string, size int) {
// Build fake SMTP message for delivery // Build fake SMTP message for delivery
testMsg := make([]byte, 0, 300) testMsg := make([]byte, 0, 300)
testMsg = append(testMsg, []byte("To: somebody@host\r\n")...) testMsg = append(testMsg, []byte("To: somebody@host\r\n")...)
@@ -407,12 +490,13 @@ func deliverMessage(ds *FileDataStore, mbName string, subject string, date time.
} }
// Create message object // Create message object
id = generateId(date) id = generateId(date)
msg := &FileMessage{ msg, err := mb.NewMessage()
mailbox: mb.(*FileMailbox), if err != nil {
writable: true, panic(err)
Fdate: date,
Fid: id,
} }
fmsg := msg.(*FileMessage)
fmsg.Fdate = date
fmsg.Fid = id
msg.Append(testMsg) msg.Append(testMsg)
if err = msg.Close(); err != nil { if err = msg.Close(); err != nil {
panic(err) panic(err)

View File

@@ -352,13 +352,18 @@ func (ss *Session) dataHandler() {
// Not our "no store" domain, so store the message // Not our "no store" domain, so store the message
mb, err := ss.server.dataStore.MailboxFor(local) mb, err := ss.server.dataStore.MailboxFor(local)
if err != nil { if err != nil {
ss.logError("Failed to open mailbox for %q", local) ss.logError("Failed to open mailbox for %q: %s", local, err)
ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", local)) ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", local))
ss.reset() ss.reset()
return return
} }
mailboxes[i] = mb mailboxes[i] = mb
messages[i] = mb.NewMessage() if messages[i], err = mb.NewMessage(); err != nil {
ss.logError("Failed to create message for %q: %s", local, err)
ss.send(fmt.Sprintf("451 Failed to create message for %v", local))
ss.reset()
return
}
// Generate Received header // Generate Received header
recd := fmt.Sprintf("Received: from %s ([%s]) by %s\r\n for <%s>; %s\r\n", recd := fmt.Sprintf("Received: from %s ([%s]) by %s\r\n for <%s>; %s\r\n",

View File

@@ -152,7 +152,7 @@ func TestMailState(t *testing.T) {
mb1 := &MockMailbox{} mb1 := &MockMailbox{}
msg1 := &MockMessage{} msg1 := &MockMessage{}
mds.On("MailboxFor").Return(mb1, nil) mds.On("MailboxFor").Return(mb1, nil)
mb1.On("NewMessage").Return(msg1) mb1.On("NewMessage").Return(msg1, nil)
msg1.On("Close").Return(nil) msg1.On("Close").Return(nil)
server, logbuf := setupSmtpServer(mds) server, logbuf := setupSmtpServer(mds)
@@ -262,7 +262,7 @@ func TestDataState(t *testing.T) {
mb1 := &MockMailbox{} mb1 := &MockMailbox{}
msg1 := &MockMessage{} msg1 := &MockMessage{}
mds.On("MailboxFor").Return(mb1, nil) mds.On("MailboxFor").Return(mb1, nil)
mb1.On("NewMessage").Return(msg1) mb1.On("NewMessage").Return(msg1, nil)
msg1.On("Close").Return(nil) msg1.On("Close").Return(nil)
server, logbuf := setupSmtpServer(mds) server, logbuf := setupSmtpServer(mds)

View File

@@ -98,9 +98,9 @@ func (m *MockMailbox) Purge() error {
return args.Error(0) return args.Error(0)
} }
func (m *MockMailbox) NewMessage() Message { func (m *MockMailbox) NewMessage() (Message, error) {
args := m.Called() args := m.Called()
return args.Get(0).(Message) return args.Get(0).(Message), args.Error(1)
} }
func (m *MockMailbox) String() string { func (m *MockMailbox) String() string {

View File

@@ -114,9 +114,9 @@ func (m *MockMailbox) Purge() error {
return args.Error(0) return args.Error(0)
} }
func (m *MockMailbox) NewMessage() smtpd.Message { func (m *MockMailbox) NewMessage() (smtpd.Message, error) {
args := m.Called() args := m.Called()
return args.Get(0).(smtpd.Message) return args.Get(0).(smtpd.Message), args.Error(1)
} }
func (m *MockMailbox) String() string { func (m *MockMailbox) String() string {