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

storage: Add Seen flag, tests for #58

This commit is contained in:
James Hillyerd
2018-03-31 21:46:10 -07:00
parent e3be5362dc
commit cc5cd7f9c3
8 changed files with 100 additions and 0 deletions

View File

@@ -16,10 +16,14 @@ This project adheres to [Semantic Versioning](http://semver.org/).
page. page.
- Debian `.deb` package generation to release process. - Debian `.deb` package generation to release process.
- RedHat `.rpm` package generation to release process. - RedHat `.rpm` package generation to release process.
- Message seen flag in REST and Web UI so you can see which messages have
already been read.
### Changed ### Changed
- Massive refactor of back-end code. Inbucket should now be both easier and - Massive refactor of back-end code. Inbucket should now be both easier and
more enjoyable to work on. more enjoyable to work on.
- Changes to file storage format, will require pre-2.0 mail store directories to
be deleted.
- Renamed `themes` directory to `ui` and eliminated the intermediate `bootstrap` - Renamed `themes` directory to `ui` and eliminated the intermediate `bootstrap`
directory. directory.
- Docker build: - Docker build:

View File

@@ -21,6 +21,7 @@ type Metadata struct {
Date time.Time Date time.Time
Subject string Subject string
Size int64 Size int64
Seen bool
} }
// Message holds both the metadata and content of a message. // Message holds both the metadata and content of a message.
@@ -109,3 +110,8 @@ func (d *Delivery) Size() int64 {
func (d *Delivery) Source() (io.ReadCloser, error) { func (d *Delivery) Source() (io.ReadCloser, error) {
return ioutil.NopCloser(d.Reader), nil return ioutil.NopCloser(d.Reader), nil
} }
// Seen getter.
func (d *Delivery) Seen() bool {
return d.Meta.Seen
}

View File

@@ -21,6 +21,7 @@ type Message struct {
Fto []*mail.Address Fto []*mail.Address
Fsubject string Fsubject string
Fsize int64 Fsize int64
Fseen bool
} }
// newMessage creates a new FileMessage object and sets the Date and ID fields. // newMessage creates a new FileMessage object and sets the Date and ID fields.
@@ -96,3 +97,8 @@ func (m *Message) Source() (reader io.ReadCloser, err error) {
} }
return file, nil return file, nil
} }
// Seen returns the seen flag value.
func (m *Message) Seen() bool {
return m.Fseen
}

View File

@@ -147,6 +147,32 @@ func (fs *Store) GetMessages(mailbox string) ([]storage.Message, error) {
return mb.getMessages() return mb.getMessages()
} }
// MarkSeen flags the message as having been read.
func (fs *Store) MarkSeen(mailbox, id string) error {
mb, err := fs.mbox(mailbox)
if err != nil {
return err
}
mb.Lock()
defer mb.Unlock()
if !mb.indexLoaded {
if err := mb.readIndex(); err != nil {
return err
}
}
for _, m := range mb.messages {
if m.Fid == id {
if m.Fseen {
// Already marked seen.
return nil
}
m.Fseen = true
break
}
}
return mb.writeIndex()
}
// RemoveMessage deletes a message by ID from the specified mailbox. // RemoveMessage deletes a message by ID from the specified mailbox.
func (fs *Store) RemoveMessage(mailbox, id string) error { func (fs *Store) RemoveMessage(mailbox, id string) error {
mb, err := fs.mbox(mailbox) mb, err := fs.mbox(mailbox)

View File

@@ -21,6 +21,7 @@ type Message struct {
date time.Time date time.Time
subject string subject string
source []byte source []byte
seen bool
el *list.Element // This message in Store.messages el *list.Element // This message in Store.messages
} }
@@ -51,3 +52,6 @@ func (m *Message) Source() (io.ReadCloser, error) {
// Size returns the message size in bytes. // Size returns the message size in bytes.
func (m *Message) Size() int64 { return int64(len(m.source)) } func (m *Message) Size() int64 { return int64(len(m.source)) }
// Seen returns the message seen flag.
func (m *Message) Seen() bool { return m.seen }

View File

@@ -112,6 +112,17 @@ func (s *Store) GetMessages(mailbox string) (ms []storage.Message, err error) {
return ms, err return ms, err
} }
// MarkSeen marks a message as having been read.
func (s *Store) MarkSeen(mailbox, id string) error {
s.withMailbox(mailbox, true, func(mb *mbox) {
m := mb.messages[id]
if m != nil {
m.seen = true
}
})
return nil
}
// PurgeMessages deletes the contents of a mailbox. // PurgeMessages deletes the contents of a mailbox.
func (s *Store) PurgeMessages(mailbox string) error { func (s *Store) PurgeMessages(mailbox string) error {
var messages map[string]*Message var messages map[string]*Message

View File

@@ -28,6 +28,7 @@ type Store interface {
AddMessage(message Message) (id string, err error) AddMessage(message Message) (id string, err error)
GetMessage(mailbox, id string) (Message, error) GetMessage(mailbox, id string) (Message, error)
GetMessages(mailbox string) ([]Message, error) GetMessages(mailbox string) ([]Message, error)
MarkSeen(mailbox, id string) error
PurgeMessages(mailbox string) error PurgeMessages(mailbox string) error
RemoveMessage(mailbox, id string) error RemoveMessage(mailbox, id string) error
VisitMailboxes(f func([]Message) (cont bool)) error VisitMailboxes(f func([]Message) (cont bool)) error
@@ -43,6 +44,7 @@ type Message interface {
Subject() string Subject() string
Source() (io.ReadCloser, error) Source() (io.ReadCloser, error)
Size() int64 Size() int64
Seen() bool
} }
// FromConfig creates an instance of the Store based on the provided configuration. // FromConfig creates an instance of the Store based on the provided configuration.

View File

@@ -28,6 +28,7 @@ func StoreSuite(t *testing.T, factory StoreFactory) {
{"content", testContent, config.Storage{}}, {"content", testContent, config.Storage{}},
{"delivery order", testDeliveryOrder, config.Storage{}}, {"delivery order", testDeliveryOrder, config.Storage{}},
{"size", testSize, config.Storage{}}, {"size", testSize, config.Storage{}},
{"seen", testSeen, config.Storage{}},
{"delete", testDelete, config.Storage{}}, {"delete", testDelete, config.Storage{}},
{"purge", testPurge, config.Storage{}}, {"purge", testPurge, config.Storage{}},
{"cap=10", testMsgCap, config.Storage{MailboxMsgCap: 10}}, {"cap=10", testMsgCap, config.Storage{MailboxMsgCap: 10}},
@@ -65,6 +66,7 @@ func testMetadata(t *testing.T, store storage.Store) {
To: to, To: to,
Date: date, Date: date,
Subject: subject, Subject: subject,
Seen: false,
}, },
Reader: strings.NewReader(content), Reader: strings.NewReader(content),
} }
@@ -107,6 +109,9 @@ func testMetadata(t *testing.T, store storage.Store) {
if sm.Size() != int64(len(content)) { if sm.Size() != int64(len(content)) {
t.Errorf("got size %v, want: %v", sm.Size(), len(content)) t.Errorf("got size %v, want: %v", sm.Size(), len(content))
} }
if sm.Seen() {
t.Errorf("got seen %v, want: false", sm.Seen())
}
} }
// testContent generates some binary content and makes sure it is correctly retrieved. // testContent generates some binary content and makes sure it is correctly retrieved.
@@ -210,6 +215,42 @@ func testSize(t *testing.T, store storage.Store) {
} }
} }
// testSeen verifies a message can be marked as seen.
func testSeen(t *testing.T, store storage.Store) {
mailbox := "lisa"
id1, _ := DeliverToStore(t, store, mailbox, "whatever", time.Now())
id2, _ := DeliverToStore(t, store, mailbox, "hello?", time.Now())
// Confirm unseen.
msg, err := store.GetMessage(mailbox, id1)
if err != nil {
t.Fatal(err)
}
if msg.Seen() {
t.Errorf("got seen %v, want: false", msg.Seen())
}
// Mark id1 seen.
err = store.MarkSeen(mailbox, id1)
if err != nil {
t.Fatal(err)
}
// Verify id1 seen.
msg, err = store.GetMessage(mailbox, id1)
if err != nil {
t.Fatal(err)
}
if !msg.Seen() {
t.Errorf("id1 got seen %v, want: true", msg.Seen())
}
// Verify id2 still unseen.
msg, err = store.GetMessage(mailbox, id2)
if err != nil {
t.Fatal(err)
}
if msg.Seen() {
t.Errorf("id2 got seen %v, want: false", msg.Seen())
}
}
// testDelete creates and deletes some messages. // testDelete creates and deletes some messages.
func testDelete(t *testing.T, store storage.Store) { func testDelete(t *testing.T, store storage.Store) {
mailbox := "fred" mailbox := "fred"