mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
Merge branch 'feature/read-58' 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:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export INBUCKET_WEB_TEMPLATECACHE="false"
|
|||||||
export INBUCKET_WEB_COOKIEAUTHKEY="not-secret"
|
export INBUCKET_WEB_COOKIEAUTHKEY="not-secret"
|
||||||
export INBUCKET_STORAGE_TYPE="file"
|
export INBUCKET_STORAGE_TYPE="file"
|
||||||
export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket"
|
export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket"
|
||||||
export INBUCKET_STORAGE_RETENTIONPERIOD="5m"
|
export INBUCKET_STORAGE_RETENTIONPERIOD="15m"
|
||||||
|
|
||||||
if ! test -x ./inbucket; then
|
if ! test -x ./inbucket; then
|
||||||
echo "$PWD/inbucket not found/executable!" >&2
|
echo "$PWD/inbucket not found/executable!" >&2
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/pkg/policy"
|
"github.com/jhillyerd/inbucket/pkg/policy"
|
||||||
"github.com/jhillyerd/inbucket/pkg/storage"
|
"github.com/jhillyerd/inbucket/pkg/storage"
|
||||||
"github.com/jhillyerd/inbucket/pkg/stringutil"
|
"github.com/jhillyerd/inbucket/pkg/stringutil"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager is the interface controllers use to interact with messages.
|
// Manager is the interface controllers use to interact with messages.
|
||||||
@@ -25,6 +26,7 @@ type Manager interface {
|
|||||||
) (id string, err error)
|
) (id string, err error)
|
||||||
GetMetadata(mailbox string) ([]*Metadata, error)
|
GetMetadata(mailbox string) ([]*Metadata, error)
|
||||||
GetMessage(mailbox, id string) (*Message, error)
|
GetMessage(mailbox, id 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
|
||||||
SourceReader(mailbox, id string) (io.ReadCloser, error)
|
SourceReader(mailbox, id string) (io.ReadCloser, error)
|
||||||
@@ -124,6 +126,13 @@ func (s *StoreManager) GetMessage(mailbox, id string) (*Message, error) {
|
|||||||
return &Message{Metadata: *header, env: env}, nil
|
return &Message{Metadata: *header, env: env}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarkSeen marks the message as having been read.
|
||||||
|
func (s *StoreManager) MarkSeen(mailbox, id string) error {
|
||||||
|
log.Debug().Str("module", "manager").Str("mailbox", mailbox).Str("id", id).
|
||||||
|
Msg("Marking as seen")
|
||||||
|
return s.Store.MarkSeen(mailbox, id)
|
||||||
|
}
|
||||||
|
|
||||||
// PurgeMessages removes all messages from the specified mailbox.
|
// PurgeMessages removes all messages from the specified mailbox.
|
||||||
func (s *StoreManager) PurgeMessages(mailbox string) error {
|
func (s *StoreManager) PurgeMessages(mailbox string) error {
|
||||||
return s.Store.PurgeMessages(mailbox)
|
return s.Store.PurgeMessages(mailbox)
|
||||||
@@ -158,5 +167,6 @@ func makeMetadata(m storage.Message) *Metadata {
|
|||||||
Date: m.Date(),
|
Date: m.Date(),
|
||||||
Subject: m.Subject(),
|
Subject: m.Subject(),
|
||||||
Size: m.Size(),
|
Size: m.Size(),
|
||||||
|
Seen: m.Seen(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/jhillyerd/inbucket/pkg/rest/model"
|
"github.com/jhillyerd/inbucket/pkg/rest/model"
|
||||||
@@ -37,6 +38,7 @@ func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (
|
|||||||
Subject: msg.Subject,
|
Subject: msg.Subject,
|
||||||
Date: msg.Date,
|
Date: msg.Date,
|
||||||
Size: msg.Size,
|
Size: msg.Size,
|
||||||
|
Seen: msg.Seen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return web.RenderJSON(w, jmessages)
|
return web.RenderJSON(w, jmessages)
|
||||||
@@ -83,6 +85,7 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (
|
|||||||
Subject: msg.Subject,
|
Subject: msg.Subject,
|
||||||
Date: msg.Date,
|
Date: msg.Date,
|
||||||
Size: msg.Size,
|
Size: msg.Size,
|
||||||
|
Seen: msg.Seen,
|
||||||
Header: msg.Header(),
|
Header: msg.Header(),
|
||||||
Body: &model.JSONMessageBodyV1{
|
Body: &model.JSONMessageBodyV1{
|
||||||
Text: msg.Text(),
|
Text: msg.Text(),
|
||||||
@@ -92,6 +95,33 @@ func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailboxMarkSeenV1 marks a message as read.
|
||||||
|
func MailboxMarkSeenV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
||||||
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
|
id := ctx.Vars["id"]
|
||||||
|
name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dec := json.NewDecoder(req.Body)
|
||||||
|
dm := model.JSONMessageHeaderV1{}
|
||||||
|
if err := dec.Decode(&dm); err != nil {
|
||||||
|
return fmt.Errorf("Failed to decode JSON: %v", err)
|
||||||
|
}
|
||||||
|
if dm.Seen {
|
||||||
|
err = ctx.Manager.MarkSeen(name, id)
|
||||||
|
if err == storage.ErrNotExist {
|
||||||
|
http.NotFound(w, req)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// This doesn't indicate empty, likely an IO error
|
||||||
|
return fmt.Errorf("MarkSeen(%q) failed: %v", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return web.RenderJSON(w, "OK")
|
||||||
|
}
|
||||||
|
|
||||||
// MailboxPurgeV1 deletes all messages from a mailbox
|
// MailboxPurgeV1 deletes all messages from a mailbox
|
||||||
func MailboxPurgeV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
func MailboxPurgeV1(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ func TestRestMailboxList(t *testing.T) {
|
|||||||
decodedStringEquals(t, result, "[0]/subject", "subject 1")
|
decodedStringEquals(t, result, "[0]/subject", "subject 1")
|
||||||
decodedStringEquals(t, result, "[0]/date", "2012-02-01T10:11:12.000000253-08:00")
|
decodedStringEquals(t, result, "[0]/date", "2012-02-01T10:11:12.000000253-08:00")
|
||||||
decodedNumberEquals(t, result, "[0]/size", 0)
|
decodedNumberEquals(t, result, "[0]/size", 0)
|
||||||
|
decodedBoolEquals(t, result, "[0]/seen", false)
|
||||||
decodedStringEquals(t, result, "[1]/mailbox", "good")
|
decodedStringEquals(t, result, "[1]/mailbox", "good")
|
||||||
decodedStringEquals(t, result, "[1]/id", "0002")
|
decodedStringEquals(t, result, "[1]/id", "0002")
|
||||||
decodedStringEquals(t, result, "[1]/from", "<from2@host>")
|
decodedStringEquals(t, result, "[1]/from", "<from2@host>")
|
||||||
@@ -122,6 +123,7 @@ func TestRestMailboxList(t *testing.T) {
|
|||||||
decodedStringEquals(t, result, "[1]/subject", "subject 2")
|
decodedStringEquals(t, result, "[1]/subject", "subject 2")
|
||||||
decodedStringEquals(t, result, "[1]/date", "2012-07-01T10:11:12.000000253-07:00")
|
decodedStringEquals(t, result, "[1]/date", "2012-07-01T10:11:12.000000253-07:00")
|
||||||
decodedNumberEquals(t, result, "[1]/size", 0)
|
decodedNumberEquals(t, result, "[1]/size", 0)
|
||||||
|
decodedBoolEquals(t, result, "[1]/seen", false)
|
||||||
|
|
||||||
if t.Failed() {
|
if t.Failed() {
|
||||||
// Wait for handler to finish logging
|
// Wait for handler to finish logging
|
||||||
@@ -183,6 +185,7 @@ func TestRestMessage(t *testing.T) {
|
|||||||
To: []*mail.Address{{Name: "", Address: "to1@host"}},
|
To: []*mail.Address{{Name: "", Address: "to1@host"}},
|
||||||
Subject: "subject 1",
|
Subject: "subject 1",
|
||||||
Date: time.Date(2012, 2, 1, 10, 11, 12, 253, tzPST),
|
Date: time.Date(2012, 2, 1, 10, 11, 12, 253, tzPST),
|
||||||
|
Seen: true,
|
||||||
},
|
},
|
||||||
&enmime.Envelope{
|
&enmime.Envelope{
|
||||||
Text: "This is some text",
|
Text: "This is some text",
|
||||||
@@ -221,6 +224,7 @@ func TestRestMessage(t *testing.T) {
|
|||||||
decodedStringEquals(t, result, "subject", "subject 1")
|
decodedStringEquals(t, result, "subject", "subject 1")
|
||||||
decodedStringEquals(t, result, "date", "2012-02-01T10:11:12.000000253-08:00")
|
decodedStringEquals(t, result, "date", "2012-02-01T10:11:12.000000253-08:00")
|
||||||
decodedNumberEquals(t, result, "size", 0)
|
decodedNumberEquals(t, result, "size", 0)
|
||||||
|
decodedBoolEquals(t, result, "seen", true)
|
||||||
decodedStringEquals(t, result, "body/text", "This is some text")
|
decodedStringEquals(t, result, "body/text", "This is some text")
|
||||||
decodedStringEquals(t, result, "body/html", "This is some HTML")
|
decodedStringEquals(t, result, "body/html", "This is some HTML")
|
||||||
decodedStringEquals(t, result, "header/To/[0]", "fred@fish.com")
|
decodedStringEquals(t, result, "header/To/[0]", "fred@fish.com")
|
||||||
@@ -234,3 +238,67 @@ func TestRestMessage(t *testing.T) {
|
|||||||
_, _ = io.Copy(os.Stderr, logbuf)
|
_, _ = io.Copy(os.Stderr, logbuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRestMarkSeen(t *testing.T) {
|
||||||
|
mm := test.NewManager()
|
||||||
|
logbuf := setupWebServer(mm)
|
||||||
|
// Create some messages.
|
||||||
|
tzPDT := time.FixedZone("PDT", -7*3600)
|
||||||
|
tzPST := time.FixedZone("PST", -8*3600)
|
||||||
|
meta1 := message.Metadata{
|
||||||
|
Mailbox: "good",
|
||||||
|
ID: "0001",
|
||||||
|
From: &mail.Address{Name: "", Address: "from1@host"},
|
||||||
|
To: []*mail.Address{{Name: "", Address: "to1@host"}},
|
||||||
|
Subject: "subject 1",
|
||||||
|
Date: time.Date(2012, 2, 1, 10, 11, 12, 253, tzPST),
|
||||||
|
}
|
||||||
|
meta2 := message.Metadata{
|
||||||
|
Mailbox: "good",
|
||||||
|
ID: "0002",
|
||||||
|
From: &mail.Address{Name: "", Address: "from2@host"},
|
||||||
|
To: []*mail.Address{{Name: "", Address: "to1@host"}},
|
||||||
|
Subject: "subject 2",
|
||||||
|
Date: time.Date(2012, 7, 1, 10, 11, 12, 253, tzPDT),
|
||||||
|
}
|
||||||
|
mm.AddMessage("good", &message.Message{Metadata: meta1})
|
||||||
|
mm.AddMessage("good", &message.Message{Metadata: meta2})
|
||||||
|
// Mark one read.
|
||||||
|
w, err := testRestPatch(baseURL+"/mailbox/good/0002", `{"seen":true}`)
|
||||||
|
expectCode := 200
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if w.Code != expectCode {
|
||||||
|
t.Fatalf("Expected code %v, got %v", expectCode, w.Code)
|
||||||
|
}
|
||||||
|
// Get mailbox.
|
||||||
|
w, err = testRestGet(baseURL + "/mailbox/good")
|
||||||
|
expectCode = 200
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if w.Code != expectCode {
|
||||||
|
t.Fatalf("Expected code %v, got %v", expectCode, w.Code)
|
||||||
|
}
|
||||||
|
// Check JSON.
|
||||||
|
dec := json.NewDecoder(w.Body)
|
||||||
|
var result []interface{}
|
||||||
|
if err := dec.Decode(&result); err != nil {
|
||||||
|
t.Errorf("Failed to decode JSON: %v", err)
|
||||||
|
}
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Fatalf("Expected 2 results, got %v", len(result))
|
||||||
|
}
|
||||||
|
decodedStringEquals(t, result, "[0]/id", "0001")
|
||||||
|
decodedBoolEquals(t, result, "[0]/seen", false)
|
||||||
|
decodedStringEquals(t, result, "[1]/id", "0002")
|
||||||
|
decodedBoolEquals(t, result, "[1]/seen", true)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,10 +58,20 @@ func (c *Client) GetMessage(name, id string) (message *Message, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarkSeen marks the specified message as having been read.
|
||||||
|
func (c *Client) MarkSeen(name, id string) error {
|
||||||
|
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
|
||||||
|
err := c.doJSON("PATCH", uri, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetMessageSource returns the message source given a mailbox name and message ID.
|
// GetMessageSource returns the message source given a mailbox name and message ID.
|
||||||
func (c *Client) GetMessageSource(name, id string) (*bytes.Buffer, error) {
|
func (c *Client) GetMessageSource(name, id string) (*bytes.Buffer, error) {
|
||||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id + "/source"
|
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id + "/source"
|
||||||
resp, err := c.do("GET", uri)
|
resp, err := c.do("GET", uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -81,7 +91,7 @@ func (c *Client) GetMessageSource(name, id string) (*bytes.Buffer, error) {
|
|||||||
// DeleteMessage deletes a single message given the mailbox name and message ID.
|
// DeleteMessage deletes a single message given the mailbox name and message ID.
|
||||||
func (c *Client) DeleteMessage(name, id string) error {
|
func (c *Client) DeleteMessage(name, id string) error {
|
||||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
|
uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
|
||||||
resp, err := c.do("DELETE", uri)
|
resp, err := c.do("DELETE", uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -95,7 +105,7 @@ func (c *Client) DeleteMessage(name, id string) error {
|
|||||||
// PurgeMailbox deletes all messages in the given mailbox
|
// PurgeMailbox deletes all messages in the given mailbox
|
||||||
func (c *Client) PurgeMailbox(name string) error {
|
func (c *Client) PurgeMailbox(name string) error {
|
||||||
uri := "/api/v1/mailbox/" + url.QueryEscape(name)
|
uri := "/api/v1/mailbox/" + url.QueryEscape(name)
|
||||||
resp, err := c.do("DELETE", uri)
|
resp, err := c.do("DELETE", uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,32 @@ func TestClientV1GetMessage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClientV1MarkSeen(t *testing.T) {
|
||||||
|
var want, got string
|
||||||
|
|
||||||
|
c, err := New(baseURLStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mth := &mockHTTPClient{}
|
||||||
|
c.client = mth
|
||||||
|
|
||||||
|
// Method under test
|
||||||
|
_ = c.MarkSeen("testbox", "20170107T224128-0000")
|
||||||
|
|
||||||
|
want = "PATCH"
|
||||||
|
got = mth.req.Method
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("req.Method == %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
want = baseURLStr + "/api/v1/mailbox/testbox/20170107T224128-0000"
|
||||||
|
got = mth.req.URL.String()
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("req.URL == %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClientV1GetMessageSource(t *testing.T) {
|
func TestClientV1GetMessageSource(t *testing.T) {
|
||||||
var want, got string
|
var want, got string
|
||||||
|
|
||||||
@@ -158,7 +184,8 @@ func TestClientV1MessageHeader(t *testing.T) {
|
|||||||
"from":"from1",
|
"from":"from1",
|
||||||
"subject":"subject1",
|
"subject":"subject1",
|
||||||
"date":"2017-01-01T00:00:00.000-07:00",
|
"date":"2017-01-01T00:00:00.000-07:00",
|
||||||
"size":100
|
"size":100,
|
||||||
|
"seen":true
|
||||||
}
|
}
|
||||||
]`
|
]`
|
||||||
|
|
||||||
@@ -216,6 +243,12 @@ func TestClientV1MessageHeader(t *testing.T) {
|
|||||||
t.Errorf("Subject == %q, want %q", got, want)
|
t.Errorf("Subject == %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wantb := true
|
||||||
|
gotb := header.Seen
|
||||||
|
if gotb != wantb {
|
||||||
|
t.Errorf("Seen == %v, want %v", gotb, wantb)
|
||||||
|
}
|
||||||
|
|
||||||
// Test MessageHeader.Delete()
|
// Test MessageHeader.Delete()
|
||||||
mth.body = ""
|
mth.body = ""
|
||||||
err = header.Delete()
|
err = header.Delete()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
@@ -18,28 +20,48 @@ type restClient struct {
|
|||||||
baseURL *url.URL
|
baseURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// do performs an HTTP request with this client and returns the response
|
// do performs an HTTP request with this client and returns the response.
|
||||||
func (c *restClient) do(method, uri string) (*http.Response, error) {
|
func (c *restClient) do(method, uri string, body []byte) (*http.Response, error) {
|
||||||
rel, err := url.Parse(uri)
|
rel, err := url.Parse(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
url := c.baseURL.ResolveReference(rel)
|
url := c.baseURL.ResolveReference(rel)
|
||||||
|
var r io.Reader
|
||||||
// Build the request
|
if body != nil {
|
||||||
req, err := http.NewRequest(method, url.String(), nil)
|
r = bytes.NewReader(body)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, url.String(), r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the request
|
|
||||||
return c.client.Do(req)
|
return c.client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// doGet performs an HTTP request with this client and marshalls the JSON response into v
|
// doJSON performs an HTTP request with this client and marshalls the JSON response into v.
|
||||||
func (c *restClient) doJSON(method string, uri string, v interface{}) error {
|
func (c *restClient) doJSON(method string, uri string, v interface{}) error {
|
||||||
resp, err := c.do(method, uri)
|
resp, err := c.do(method, uri, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Decode response body
|
||||||
|
return json.NewDecoder(resp.Body).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// doJSONBody performs an HTTP request with this client and marshalls the JSON response into v.
|
||||||
|
func (c *restClient) doJSONBody(method string, uri string, body []byte, v interface{}) error {
|
||||||
|
resp, err := c.do(method, uri, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,29 @@ func (m *mockHTTPClient) Do(req *http.Request) (resp *http.Response, err error)
|
|||||||
StatusCode: m.statusCode,
|
StatusCode: m.statusCode,
|
||||||
Body: ioutil.NopCloser(bytes.NewBufferString(m.body)),
|
Body: ioutil.NopCloser(bytes.NewBufferString(m.body)),
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockHTTPClient) ReqBody() []byte {
|
||||||
|
r, err := m.req.GetBody()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = r.Close()
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
func TestDo(t *testing.T) {
|
func TestDo(t *testing.T) {
|
||||||
var want, got string
|
var want, got string
|
||||||
|
|
||||||
mth := &mockHTTPClient{}
|
mth := &mockHTTPClient{}
|
||||||
c := &restClient{mth, baseURL}
|
c := &restClient{mth, baseURL}
|
||||||
|
body := []byte("Test body")
|
||||||
|
|
||||||
_, err := c.do("POST", "/dopost")
|
_, err := c.do("POST", "/dopost", body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -61,6 +73,11 @@ func TestDo(t *testing.T) {
|
|||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("req.URL == %q, want %q", got, want)
|
t.Errorf("req.URL == %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b := mth.ReqBody()
|
||||||
|
if !bytes.Equal(b, body) {
|
||||||
|
t.Errorf("req.Body == %q, want %q", b, body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDoJSON(t *testing.T) {
|
func TestDoJSON(t *testing.T) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type JSONMessageHeaderV1 struct {
|
|||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
Seen bool `json:"seen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONMessageV1 contains the same data as the header plus a JSONMessageBody
|
// JSONMessageV1 contains the same data as the header plus a JSONMessageBody
|
||||||
@@ -24,6 +25,7 @@ type JSONMessageV1 struct {
|
|||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
Seen bool `json:"seen"`
|
||||||
Body *JSONMessageBodyV1 `json:"body"`
|
Body *JSONMessageBodyV1 `json:"body"`
|
||||||
Header map[string][]string `json:"header"`
|
Header map[string][]string `json:"header"`
|
||||||
Attachments []*JSONMessageAttachmentV1 `json:"attachments"`
|
Attachments []*JSONMessageAttachmentV1 `json:"attachments"`
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ func SetupRoutes(r *mux.Router) {
|
|||||||
web.Handler(MailboxPurgeV1)).Name("MailboxPurgeV1").Methods("DELETE")
|
web.Handler(MailboxPurgeV1)).Name("MailboxPurgeV1").Methods("DELETE")
|
||||||
r.Path("/api/v1/mailbox/{name}/{id}").Handler(
|
r.Path("/api/v1/mailbox/{name}/{id}").Handler(
|
||||||
web.Handler(MailboxShowV1)).Name("MailboxShowV1").Methods("GET")
|
web.Handler(MailboxShowV1)).Name("MailboxShowV1").Methods("GET")
|
||||||
|
r.Path("/api/v1/mailbox/{name}/{id}").Handler(
|
||||||
|
web.Handler(MailboxMarkSeenV1)).Name("MailboxMarkSeenV1").Methods("PATCH")
|
||||||
r.Path("/api/v1/mailbox/{name}/{id}").Handler(
|
r.Path("/api/v1/mailbox/{name}/{id}").Handler(
|
||||||
web.Handler(MailboxDeleteV1)).Name("MailboxDeleteV1").Methods("DELETE")
|
web.Handler(MailboxDeleteV1)).Name("MailboxDeleteV1").Methods("DELETE")
|
||||||
r.Path("/api/v1/mailbox/{name}/{id}/source").Handler(
|
r.Path("/api/v1/mailbox/{name}/{id}/source").Handler(
|
||||||
|
|||||||
@@ -21,7 +21,17 @@ func testRestGet(url string) (*httptest.ResponseRecorder, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
web.Router.ServeHTTP(w, req)
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRestPatch(url string, body string) (*httptest.ResponseRecorder, error) {
|
||||||
|
req, err := http.NewRequest("PATCH", url, strings.NewReader(body))
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
web.Router.ServeHTTP(w, req)
|
web.Router.ServeHTTP(w, req)
|
||||||
return w, nil
|
return w, nil
|
||||||
@@ -46,6 +56,22 @@ func setupWebServer(mm message.Manager) *bytes.Buffer {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodedBoolEquals(t *testing.T, json interface{}, path string, want bool) {
|
||||||
|
t.Helper()
|
||||||
|
els := strings.Split(path, "/")
|
||||||
|
val, msg := getDecodedPath(json, els...)
|
||||||
|
if msg != "" {
|
||||||
|
t.Errorf("JSON result%s", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got, ok := val.(bool); ok {
|
||||||
|
if got == want {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Errorf("JSON result/%s == %v (%T), want: %v", path, val, val, want)
|
||||||
|
}
|
||||||
|
|
||||||
func decodedNumberEquals(t *testing.T, json interface{}, path string, want float64) {
|
func decodedNumberEquals(t *testing.T, json interface{}, path string, want float64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
els := strings.Split(path, "/")
|
els := strings.Split(path, "/")
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -57,3 +57,17 @@ func (m *ManagerStub) GetMetadata(mailbox string) ([]*message.Metadata, error) {
|
|||||||
func (m *ManagerStub) MailboxForAddress(address string) (string, error) {
|
func (m *ManagerStub) MailboxForAddress(address string) (string, error) {
|
||||||
return policy.ParseMailboxName(address)
|
return policy.ParseMailboxName(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarkSeen marks a message as having been read.
|
||||||
|
func (m *ManagerStub) MarkSeen(mailbox, id string) error {
|
||||||
|
if mailbox == "messageerr" {
|
||||||
|
return errors.New("internal error")
|
||||||
|
}
|
||||||
|
for _, msg := range m.mailboxes[mailbox] {
|
||||||
|
if msg.ID == id {
|
||||||
|
msg.Metadata.Seen = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return storage.ErrNotExist
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ body {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-list-entry .unseen {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.message-list-scroll {
|
.message-list-scroll {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ var messageListMargin = 275;
|
|||||||
var clipboard = null;
|
var clipboard = null;
|
||||||
var messageListScroll = false;
|
var messageListScroll = false;
|
||||||
var messageListData = null;
|
var messageListData = null;
|
||||||
|
var seenDelay = null;
|
||||||
|
|
||||||
// clearMessageSearch resets the message list search
|
// clearMessageSearch resets the message list search
|
||||||
function clearMessageSearch() {
|
function clearMessageSearch() {
|
||||||
@@ -55,8 +56,11 @@ function loadList() {
|
|||||||
url: '/api/v1/mailbox/' + mailbox,
|
url: '/api/v1/mailbox/' + mailbox,
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
messageListData = data.reverse();
|
messageListData = data.reverse();
|
||||||
|
for (i=0; i<messageListData.length; i++) {
|
||||||
|
messageListData[i].seenClass = messageListData[i].seen ? '' : 'unseen'
|
||||||
|
}
|
||||||
// Render list
|
// Render list
|
||||||
$('#message-list').loadTemplate($('#list-entry-template'), data);
|
$('#message-list').loadTemplate($('#list-entry-template'), messageListData);
|
||||||
$('.message-list-entry').click(onMessageListClick);
|
$('.message-list-entry').click(onMessageListClick);
|
||||||
// Reveal and select current message
|
// Reveal and select current message
|
||||||
$("#message-list").slideDown();
|
$("#message-list").slideDown();
|
||||||
@@ -115,6 +119,7 @@ function onDocumentChange() {
|
|||||||
|
|
||||||
// onDocumentReady is called by mailbox/index.html to initialize
|
// onDocumentReady is called by mailbox/index.html to initialize
|
||||||
function onDocumentReady() {
|
function onDocumentReady() {
|
||||||
|
seenDelay = makeDelay(1500);
|
||||||
// Prevent search and resize handlers being called too often
|
// Prevent search and resize handlers being called too often
|
||||||
var searchDelay = makeDelay(200);
|
var searchDelay = makeDelay(200);
|
||||||
var resizeDelay = makeDelay(100);
|
var resizeDelay = makeDelay(100);
|
||||||
@@ -142,10 +147,12 @@ function onDocumentReady() {
|
|||||||
|
|
||||||
// onMessageListClick is triggered by clicks on the message list
|
// onMessageListClick is triggered by clicks on the message list
|
||||||
function onMessageListClick() {
|
function onMessageListClick() {
|
||||||
|
var id = this.id;
|
||||||
$('.message-list-entry').removeClass("disabled");
|
$('.message-list-entry').removeClass("disabled");
|
||||||
$(this).addClass("disabled");
|
$(this).addClass("disabled");
|
||||||
$('#message-content').load('/mailbox/' + mailbox + '/' + this.id, onMessageLoaded);
|
$('#message-content').load('/mailbox/' + mailbox + '/' + id, onMessageLoaded);
|
||||||
selected = this.id;
|
selected = id;
|
||||||
|
seenDelay(function() { markSeen(id); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// onMessageLoaded is called each time a new message is shown
|
// onMessageLoaded is called each time a new message is shown
|
||||||
@@ -199,3 +206,26 @@ function updateMessageSearch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// markSeen notifies the server that this message has been seen by the user.
|
||||||
|
function markSeen(id) {
|
||||||
|
for (i=0; i<messageListData.length; i++) {
|
||||||
|
if (messageListData[i].id == id) {
|
||||||
|
if (messageListData[i].seen) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
messageListData[i].seen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var updateClass = function() {
|
||||||
|
$('#' + id).find('.unseen').removeClass('unseen');
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: 'PATCH',
|
||||||
|
url: '/api/v1/mailbox/' + mailbox + '/' + id,
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({'seen': true}),
|
||||||
|
processData: false,
|
||||||
|
success: updateClass
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ $(document).ready(function() {
|
|||||||
<script type="text/html" id="list-entry-template">
|
<script type="text/html" id="list-entry-template">
|
||||||
<button data-id="id" type="button" class="message-list-entry list-group-item">
|
<button data-id="id" type="button" class="message-list-entry list-group-item">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-4 col-md-12 text-primary" data-content-text="subject"
|
<div class="col-sm-4 col-md-12 text-primary">
|
||||||
data-format="subject"/>
|
<span data-class="seenClass" data-content-text="subject" data-format="subject"/>
|
||||||
|
</div>
|
||||||
<div class="col-sm-4 col-md-12 small" data-content-text="from"/>
|
<div class="col-sm-4 col-md-12 small" data-content-text="from"/>
|
||||||
<div class="col-sm-4 col-md-12 small" data-content="date" data-format="date"/>
|
<div class="col-sm-4 col-md-12 small" data-content="date" data-format="date"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user