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:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user