mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 17:44:35 +03:00
implement imap savedate extension, rfc 8514
it makes a new field available on stored messages. not when they they were received (over smtp) or appended to the mailbox (over imap), but when they were last "saved" in the mailbox. copy/move of a message (eg to the trash) resets the "savedate" value. this helps implement "remove messages from trash after X days".
This commit is contained in:
@ -404,6 +404,17 @@ func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token {
|
||||
m := cmd.xensureMessage()
|
||||
return []token{bare("INTERNALDATE"), dquote(m.Received.Format("_2-Jan-2006 15:04:05 -0700"))}
|
||||
|
||||
case "SAVEDATE":
|
||||
m := cmd.xensureMessage()
|
||||
// For messages in storage from before we implemented this extension, we don't have
|
||||
// a savedate, and we return nil. This is normally meant to be per mailbox, but
|
||||
// returning it per message should be fine. ../rfc/8514:191
|
||||
var savedate token = nilt
|
||||
if m.SaveDate != nil {
|
||||
savedate = dquote(m.SaveDate.Format("_2-Jan-2006 15:04:05 -0700"))
|
||||
}
|
||||
return []token{bare("SAVEDATE"), savedate}
|
||||
|
||||
case "BODYSTRUCTURE":
|
||||
_, part := cmd.xensureParsed()
|
||||
bs := xbodystructure(part, true)
|
||||
|
@ -5,7 +5,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mjl-/bstore"
|
||||
|
||||
"github.com/mjl-/mox/imapclient"
|
||||
"github.com/mjl-/mox/store"
|
||||
)
|
||||
|
||||
func TestFetch(t *testing.T) {
|
||||
@ -201,6 +204,27 @@ func TestFetch(t *testing.T) {
|
||||
tc.transactf("ok", "uid fetch 2 body[]")
|
||||
tc.xuntagged()
|
||||
|
||||
// SAVEDATE
|
||||
tc.transactf("ok", "uid fetch 1 savedate")
|
||||
// Fetch exact SaveDate we'll be expecting from server.
|
||||
var saveDate time.Time
|
||||
err = tc.account.DB.Read(ctxbg, func(tx *bstore.Tx) error {
|
||||
inbox, err := tc.account.MailboxFind(tx, "Inbox")
|
||||
tc.check(err, "get inbox")
|
||||
if inbox == nil {
|
||||
t.Fatalf("missing inbox")
|
||||
}
|
||||
m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: inbox.ID, UID: store.UID(uid1)}).Get()
|
||||
tc.check(err, "get message")
|
||||
if m.SaveDate == nil {
|
||||
t.Fatalf("zero savedate for message")
|
||||
}
|
||||
saveDate = m.SaveDate.Truncate(time.Second)
|
||||
return nil
|
||||
})
|
||||
tc.check(err, "get savedate")
|
||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchSaveDate{SaveDate: &saveDate}}})
|
||||
|
||||
// Test some invalid syntax.
|
||||
tc.transactf("bad", "fetch")
|
||||
tc.transactf("bad", "fetch ")
|
||||
|
@ -575,7 +575,8 @@ func (p *parser) xsectionBinary() (r []uint32) {
|
||||
var fetchAttWords = []string{
|
||||
"ENVELOPE", "FLAGS", "INTERNALDATE", "RFC822.SIZE", "BODYSTRUCTURE", "UID", "BODY.PEEK", "BODY", "BINARY.PEEK", "BINARY.SIZE", "BINARY",
|
||||
"RFC822.HEADER", "RFC822.TEXT", "RFC822", // older IMAP
|
||||
"MODSEQ", // CONDSTORE extension.
|
||||
"MODSEQ", // CONDSTORE extension.
|
||||
"SAVEDATE", // SAVEDATE extension, ../rfc/8514:186
|
||||
}
|
||||
|
||||
// ../rfc/9051:6557 ../rfc/3501:4751 ../rfc/7162:2483
|
||||
@ -803,7 +804,8 @@ var searchKeyWords = []string{
|
||||
"SENTBEFORE", "SENTON",
|
||||
"SENTSINCE", "SMALLER",
|
||||
"UID", "UNDRAFT",
|
||||
"MODSEQ", // CONDSTORE extension.
|
||||
"MODSEQ", // CONDSTORE extension.
|
||||
"SAVEDBEFORE", "SAVEDON", "SAVEDSINCE", "SAVEDATESUPPORTED", // SAVEDATE extension, ../rfc/8514:203
|
||||
}
|
||||
|
||||
// ../rfc/9051:6923 ../rfc/3501:4957, MODSEQ ../rfc/7162:2492
|
||||
@ -927,6 +929,10 @@ func (p *parser) xsearchKey() *searchKey {
|
||||
sk.clientModseq = &v
|
||||
// MODSEQ is a CONDSTORE-enabling parameter. ../rfc/7162:377
|
||||
p.conn.enabled[capCondstore] = true
|
||||
case "SAVEDBEFORE", "SAVEDON", "SAVEDSINCE":
|
||||
p.xspace()
|
||||
sk.date = p.xdate() // ../rfc/8514:267
|
||||
case "SAVEDATESUPPORTED":
|
||||
default:
|
||||
p.xerrorf("missing case for op %q", sk.op)
|
||||
}
|
||||
|
@ -514,6 +514,31 @@ func (s *search) match0(sk searchKey) bool {
|
||||
case "MODSEQ":
|
||||
// ../rfc/7162:1045
|
||||
return s.m.ModSeq.Client() >= *sk.clientModseq
|
||||
case "SAVEDBEFORE", "SAVEDON", "SAVEDSINCE":
|
||||
// If we don't have a savedate for this message (for messages received before we
|
||||
// implemented this feature), we use the "internal date" (received timestamp) of
|
||||
// the message. ../rfc/8514:237
|
||||
rt := s.m.Received
|
||||
if s.m.SaveDate != nil {
|
||||
rt = *s.m.SaveDate
|
||||
}
|
||||
|
||||
skdt := sk.date.Format("2006-01-02")
|
||||
rdt := rt.Format("2006-01-02")
|
||||
switch sk.op {
|
||||
case "SAVEDBEFORE":
|
||||
return rdt < skdt
|
||||
case "SAVEDON":
|
||||
return rdt == skdt
|
||||
case "SAVEDSINCE":
|
||||
return rdt >= skdt
|
||||
}
|
||||
panic("missing case")
|
||||
case "SAVEDATESUPPORTED":
|
||||
// We return whether we have a savedate for this message. We support it on all
|
||||
// mailboxes, but we only have this metadata from the time we implemented this
|
||||
// feature.
|
||||
return s.m.SaveDate != nil
|
||||
}
|
||||
|
||||
if s.p == nil {
|
||||
|
@ -65,6 +65,7 @@ func TestSearch(t *testing.T) {
|
||||
|
||||
// Add 5 and delete first 4 messages. So UIDs start at 5.
|
||||
received := time.Date(2020, time.January, 1, 10, 0, 0, 0, time.UTC)
|
||||
saveDate := time.Now()
|
||||
for i := 0; i < 5; i++ {
|
||||
tc.client.Append("inbox", nil, &received, []byte(exampleMsg))
|
||||
}
|
||||
@ -110,6 +111,20 @@ func TestSearch(t *testing.T) {
|
||||
tc.transactf("ok", "search before 1-Jan-2020")
|
||||
tc.xsearch() // Before is about received, not date header of message.
|
||||
|
||||
// SAVEDATE extension.
|
||||
tc.transactf("ok", "search savedbefore %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search savedbefore %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", "search savedon %s", saveDate.Format("2-Jan-2006"))
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search savedon %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch()
|
||||
tc.transactf("ok", "search savedsince %s", saveDate.Add(-24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch(1, 2, 3)
|
||||
tc.transactf("ok", "search savedsince %s", saveDate.Add(24*time.Hour).Format("2-Jan-2006"))
|
||||
tc.xsearch()
|
||||
|
||||
tc.transactf("ok", `search body "Joe"`)
|
||||
tc.xsearch(1)
|
||||
tc.transactf("ok", `search body "Joe" body "bogus"`)
|
||||
|
@ -159,12 +159,13 @@ var authFailDelay = time.Second // After authentication failure.
|
||||
// STATUS=SIZE: ../rfc/8438 ../rfc/9051:8024
|
||||
// QUOTA QUOTA=RES-STORAGE: ../rfc/9208:111
|
||||
// METADATA: ../rfc/5464
|
||||
// SAVEDATE: ../rfc/8514
|
||||
//
|
||||
// We always announce support for SCRAM PLUS-variants, also on connections without
|
||||
// TLS. The client should not be selecting PLUS variants on non-TLS connections,
|
||||
// instead opting to do the bare SCRAM variant without indicating the server claims
|
||||
// to support the PLUS variant (skipping the server downgrade detection check).
|
||||
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC STATUS=SIZE QUOTA QUOTA=RES-STORAGE METADATA"
|
||||
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC STATUS=SIZE QUOTA QUOTA=RES-STORAGE METADATA SAVEDATE"
|
||||
|
||||
type conn struct {
|
||||
cid int64
|
||||
@ -3868,6 +3869,7 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
||||
conf, _ := c.account.Conf()
|
||||
|
||||
mbKeywords := map[string]struct{}{}
|
||||
now := time.Now()
|
||||
|
||||
// Insert new messages into database.
|
||||
var origMsgIDs, newMsgIDs []int64
|
||||
@ -3891,6 +3893,7 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
||||
}
|
||||
m.TrainedJunk = nil
|
||||
m.JunkFlagsForMailbox(mbDst, conf)
|
||||
m.SaveDate = &now
|
||||
err := tx.Insert(&m)
|
||||
xcheckf(err, "inserting message")
|
||||
msgs[uid] = m
|
||||
@ -4033,6 +4036,7 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
||||
}
|
||||
|
||||
keywords := map[string]struct{}{}
|
||||
now := time.Now()
|
||||
|
||||
conf, _ := c.account.Conf()
|
||||
for i := range msgs {
|
||||
@ -4061,6 +4065,7 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
||||
m.UID = uidnext
|
||||
m.ModSeq = modseq
|
||||
m.JunkFlagsForMailbox(mbDst, conf)
|
||||
m.SaveDate = &now
|
||||
uidnext++
|
||||
err := tx.Update(m)
|
||||
xcheckf(err, "updating moved message in database")
|
||||
|
Reference in New Issue
Block a user