diff --git a/smtpd/datastore.go b/smtpd/datastore.go index 79bde52..f2ec9e2 100644 --- a/smtpd/datastore.go +++ b/smtpd/datastore.go @@ -17,6 +17,7 @@ import ( type DataStore interface { MailboxFor(emailAddress string) (Mailbox, error) + AllMailboxes() ([]Mailbox, error) } type Mailbox interface { @@ -95,6 +96,11 @@ func (ds *FileDataStore) MailboxFor(emailAddress string) (Mailbox, error) { return &FileMailbox{store: ds, name: name, dirName: dir, path: path}, nil } +// AllMailboxes returns a slice with all Mailboxes +func (ds *FileDataStore) AllMailboxes() ([]Mailbox, error) { + return nil, nil +} + // A Mailbox manages the mail for a specific user and correlates to a particular // directory on disk. type FileMailbox struct { diff --git a/smtpd/retention.go b/smtpd/retention.go index 89e8b60..ddfe260 100644 --- a/smtpd/retention.go +++ b/smtpd/retention.go @@ -1,12 +1,37 @@ package smtpd import ( - //"github.com/jhillyerd/inbucket/config" - //"github.com/jhillyerd/inbucket/log" -// "io/ioutil" + "github.com/jhillyerd/inbucket/log" + "time" ) // retentionScan does a single pass of all mailboxes looking for messages that can be purged -func retentionScan(ds *DataStore) { +func retentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) error { + log.Trace("Starting retention scan") + cutoff := time.Now().Add(-1 * maxAge) + mboxes, err := ds.AllMailboxes() + if err != nil { + return err + } + for _, mb := range mboxes { + messages, err := mb.GetMessages() + if err != nil { + return err + } + for _, msg := range messages { + if msg.Date().Before(cutoff) { + log.Trace("Purging expired message %v", msg.Id()) + err = msg.Delete() + if err != nil { + // Log but don't abort + log.Error("Failed to purge message %v: %v", msg.Id(), err) + } + } + } + // Sleep after completing a mailbox + time.Sleep(sleep) + } + + return nil } diff --git a/smtpd/retention_test.go b/smtpd/retention_test.go index c842472..2d39fdd 100644 --- a/smtpd/retention_test.go +++ b/smtpd/retention_test.go @@ -2,123 +2,160 @@ package smtpd import ( "fmt" - "github.com/stretchrcom/testify/assert" "github.com/stretchrcom/testify/mock" - "io/ioutil" - "os" - "path/filepath" + "net/mail" "testing" "time" ) +func TestSometing(t *testing.T) { + // Create mock objects + mds := &MockDataStore{} + + mb1 := &MockMailbox{} + mb2 := &MockMailbox{} + mb3 := &MockMailbox{} + + // Mockup some different aged messages (num is in hours) + new1 := mockMessage(0) + new2 := mockMessage(1) + new3 := mockMessage(2) + old1 := mockMessage(4) + old2 := mockMessage(12) + old3 := mockMessage(24) + + // First it should ask for all mailboxes + mds.On("AllMailboxes").Return([]Mailbox{mb1, mb2, mb3}, nil) + + // Then for all messages on each box + mb1.On("GetMessages").Return([]Message{new1, old1, old2}, nil) + mb2.On("GetMessages").Return([]Message{old3, new2}, nil) + mb3.On("GetMessages").Return([]Message{new3}, nil) + + // Test 4 hour retention + retentionScan(mds, 4*time.Hour, 0) + + // Check our assertions + mds.AssertExpectations(t) + mb1.AssertExpectations(t) + mb2.AssertExpectations(t) + mb3.AssertExpectations(t) + + // Delete should not have been called on new messages + new1.AssertNotCalled(t, "Delete") + new2.AssertNotCalled(t, "Delete") + new3.AssertNotCalled(t, "Delete") + + // Delete should have been called once on old messages + old1.AssertNumberOfCalls(t, "Delete", 1) + old2.AssertNumberOfCalls(t, "Delete", 1) + old3.AssertNumberOfCalls(t, "Delete", 1) +} + +// Make a MockMessage of a specific age +func mockMessage(ageHours int) *MockMessage { + msg := &MockMessage{} + msg.On("Id").Return(fmt.Sprintf("MSG[age=%vh]", ageHours)) + msg.On("Date").Return(time.Now().Add(time.Duration(ageHours*-1) * time.Hour)) + msg.On("Delete").Return(nil) + return msg +} + +// Mock DataStore object type MockDataStore struct { mock.Mock - path string - mailPath string } -var names = []string{"abby", "bill", "christa", "donald", "evelyn"} - -func TestSometing(t *testing.T) { - ds := setupDataStore() - fmt.Println(ds) - //defer teardownDataStore(ds) - - assert.Equal(t, true, true) +func (m *MockDataStore) MailboxFor(name string) (Mailbox, error) { + return nil, nil } -// setupDataStore will build the following structure in a temporary -// directory: -// -// /tmp/inbucket????????? -// └── mail -// ├── 53e -// │   └── 53e11e -// │   └── 53e11eb7b24cc39e33733a0ff06640f1b39425ea -// │   ├── 20121024T164239-0000.gob -// │   ├── 20121024T164239-0000.raw -// │   ├── 20121025T164239-0000.gob -// │   └── 20121025T164239-0000.raw -// ├── 60c -// │   └── 60c596 -// │   └── 60c5963a56da1425f133d28166ca4fe70dcb25f5 -// │   ├── 20121024T164239-0000.gob -// │   ├── 20121024T164239-0000.raw -// │   ├── 20121025T164239-0000.gob -// │   └── 20121025T164239-0000.raw -// ├── 88d -// │   └── 88db92 -// │   └── 88db9292c772b38311e1778f6f6b18216443abf0 -// │   ├── 20121024T164239-0000.gob -// │   ├── 20121024T164239-0000.raw -// │   ├── 20121025T164239-0000.gob -// │   └── 20121025T164239-0000.raw -// ├── c69 -// │   └── c692d6 -// │   └── c692d6a10598e0a801576fdd4ecf3c37e45bfbc4 -// │   ├── 20121024T164239-0000.gob -// │   ├── 20121024T164239-0000.raw -// │   ├── 20121025T164239-0000.gob -// │   └── 20121025T164239-0000.raw -// └── e76 -// └── e76cef -// └── e76ceff3c47adb10f62b1acd7109f88fbd5e9ca7 -// ├── 20121024T164239-0000.gob -// ├── 20121024T164239-0000.raw -// ├── 20121025T164239-0000.gob -// └── 20121025T164239-0000.raw -func setupDataStore() *FileDataStore { - // Build fake SMTP message for delivery - testMsg := make([]byte, 0, 300) - testMsg = append(testMsg, []byte("To: somebody@host\r\n")...) - testMsg = append(testMsg, []byte("From: somebodyelse@host\r\n")...) - testMsg = append(testMsg, []byte("Subject: test message\r\n")...) - testMsg = append(testMsg, []byte("\r\n")...) - testMsg = append(testMsg, []byte("Test Body\r\n")...) - - path, err := ioutil.TempDir("", "inbucket") - if err != nil { - panic(err) - } - mailPath := filepath.Join(path, "mail") - ds := &FileDataStore{path: path, mailPath: mailPath} - - for _, name := range names { - mb, err := ds.MailboxFor(name) - if err != nil { - panic(err) - } - // Create day old message - date := time.Now().Add(-24 * time.Hour) - msg := &FileMessage{ - mailbox: mb.(*FileMailbox), - writable: true, - Fdate: date, - Fid: generatePrefix(date) + "-0000", - } - msg.Append(testMsg) - if err = msg.Close(); err != nil { - panic(err) - } - - // Create current message - date = time.Now() - msg = &FileMessage{ - mailbox: mb.(*FileMailbox), - writable: true, - Fdate: date, - Fid: generatePrefix(date) + "-0000", - } - msg.Append(testMsg) - if err = msg.Close(); err != nil { - panic(err) - } - } - return ds +func (m *MockDataStore) AllMailboxes() ([]Mailbox, error) { + args := m.Called() + return args.Get(0).([]Mailbox), args.Error(1) } -func teardownDataStore(ds *FileDataStore) { - if err := os.RemoveAll(ds.path); err != nil { - panic(err) - } +// Mock Mailbox object +type MockMailbox struct { + mock.Mock +} + +func (m *MockMailbox) GetMessages() ([]Message, error) { + args := m.Called() + return args.Get(0).([]Message), args.Error(1) +} + +func (m *MockMailbox) GetMessage(id string) (Message, error) { + args := m.Called(id) + return args.Get(0).(Message), args.Error(1) +} + +func (m *MockMailbox) NewMessage() Message { + args := m.Called() + return args.Get(0).(Message) +} + +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() (msg *mail.Message, body *MIMEBody, err error) { + args := m.Called() + return args.Get(0).(*mail.Message), args.Get(1).(*MIMEBody), args.Error(2) +} + +func (m *MockMessage) ReadRaw() (raw *string, err error) { + args := m.Called() + return args.Get(0).(*string), args.Error(1) +} + +func (m *MockMessage) Append(data []byte) error { + args := m.Called(data) + return args.Error(0) +} + +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) }