mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 08:58:16 +03:00
imapclient: add a type Append for messages for the APPEND-command, and accept multiple for servers with MULTIAPPEND capability
and a few nits.
This commit is contained in:
parent
88a68e9143
commit
1066eb4c9f
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -265,14 +266,50 @@ func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (untagged []Untagged,
|
|||||||
return c.Transactf("status %s (%s)", astring(mailbox), strings.Join(l, " "))
|
return c.Transactf("status %s (%s)", astring(mailbox), strings.Join(l, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append represents a parameter to the APPEND or REPLACE commands.
|
||||||
|
type Append struct {
|
||||||
|
Flags []string
|
||||||
|
Received *time.Time
|
||||||
|
Size int64
|
||||||
|
Data io.Reader // Must return Size bytes.
|
||||||
|
}
|
||||||
|
|
||||||
// Append adds message to mailbox with flags and optional receive time.
|
// Append adds message to mailbox with flags and optional receive time.
|
||||||
func (c *Conn) Append(mailbox string, flags []string, received *time.Time, message []byte) (untagged []Untagged, result Result, rerr error) {
|
//
|
||||||
|
// Multiple messages are only possible when the server has announced the
|
||||||
|
// MULTIAPPEND capability.
|
||||||
|
func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged []Untagged, result Result, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr)
|
||||||
var date string
|
|
||||||
if received != nil {
|
if _, ok := c.CapAvailable[CapMultiAppend]; !ok && len(more) > 0 {
|
||||||
date = ` "` + received.Format("_2-Jan-2006 15:04:05 -0700") + `"`
|
c.xerrorf("can only append multiple messages when server has announced MULTIAPPEND capability")
|
||||||
}
|
}
|
||||||
return c.Transactf("append %s (%s)%s {%d+}\r\n%s", astring(mailbox), strings.Join(flags, " "), date, len(message), message)
|
|
||||||
|
tag := c.nextTag()
|
||||||
|
c.LastTag = tag
|
||||||
|
|
||||||
|
_, err := fmt.Fprintf(c.bw, "%s append %s", tag, astring(mailbox))
|
||||||
|
c.xcheckf(err, "write command")
|
||||||
|
|
||||||
|
msgs := append([]Append{message}, more...)
|
||||||
|
for _, m := range msgs {
|
||||||
|
var date string
|
||||||
|
if m.Received != nil {
|
||||||
|
date = ` "` + m.Received.Format("_2-Jan-2006 15:04:05 -0700") + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: use literal8 if needed, with "UTF8()" if required.
|
||||||
|
// todo: for larger messages, use a synchronizing literal.
|
||||||
|
|
||||||
|
fmt.Fprintf(c.bw, " (%s)%s {%d+}\r\n", strings.Join(m.Flags, " "), date, m.Size)
|
||||||
|
c.xflush()
|
||||||
|
_, err := io.Copy(c, m.Data)
|
||||||
|
c.xcheckf(err, "write message data")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(c.bw, "\r\n")
|
||||||
|
c.xflush()
|
||||||
|
return c.Response()
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: No idle command. Idle is better implemented by writing the request and reading and handling the responses as they come in.
|
// note: No idle command. Idle is better implemented by writing the request and reading and handling the responses as they come in.
|
||||||
|
@ -342,7 +342,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable("imap4rev2")
|
||||||
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
||||||
tc.check(err, "parse time")
|
tc.check(err, "parse time")
|
||||||
tc.client.Append("inbox", nil, &received, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
|
@ -25,12 +25,12 @@ func TestCopy(t *testing.T) {
|
|||||||
tc.transactf("bad", "copy 1 inbox ") // Leftover.
|
tc.transactf("bad", "copy 1 inbox ") // Leftover.
|
||||||
|
|
||||||
// Seqs 1,2 and UIDs 3,4.
|
// Seqs 1,2 and UIDs 3,4.
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.StoreFlagsSet("1:2", true, `\Deleted`)
|
tc.client.StoreFlagsSet("1:2", true, `\Deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
|
|
||||||
tc.transactf("no", "copy 1 nonexistent")
|
tc.transactf("no", "copy 1 nonexistent")
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcode("TRYCREATE")
|
||||||
|
@ -54,7 +54,7 @@ func TestDelete(t *testing.T) {
|
|||||||
|
|
||||||
// Let's try again with a message present.
|
// Let's try again with a message present.
|
||||||
tc.client.Create("msgs", nil)
|
tc.client.Create("msgs", nil)
|
||||||
tc.client.Append("msgs", nil, nil, []byte(exampleMsg))
|
tc.client.Append("msgs", makeAppend(exampleMsg))
|
||||||
tc.transactf("ok", "delete msgs")
|
tc.transactf("ok", "delete msgs")
|
||||||
|
|
||||||
// Delete for inbox/* is allowed.
|
// Delete for inbox/* is allowed.
|
||||||
|
@ -31,9 +31,9 @@ func TestExpunge(t *testing.T) {
|
|||||||
|
|
||||||
tc.client.Unselect()
|
tc.client.Unselect()
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.transactf("ok", "expunge") // Still nothing to remove.
|
tc.transactf("ok", "expunge") // Still nothing to remove.
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
|
||||||
@ -51,9 +51,9 @@ func TestExpunge(t *testing.T) {
|
|||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
|
|
||||||
// Only UID 2 is still left. We'll add 3 more. Getting us to UIDs 2,4,5,6.
|
// Only UID 2 is still left. We'll add 3 more. Getting us to UIDs 2,4,5,6.
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
|
|
||||||
tc.transactf("bad", "uid expunge") // Missing uid set.
|
tc.transactf("bad", "uid expunge") // Missing uid set.
|
||||||
tc.transactf("bad", "uid expunge 1 leftover") // Leftover data.
|
tc.transactf("bad", "uid expunge 1 leftover") // Leftover data.
|
||||||
|
@ -19,7 +19,7 @@ func TestFetch(t *testing.T) {
|
|||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable("imap4rev2")
|
||||||
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
||||||
tc.check(err, "parse time")
|
tc.check(err, "parse time")
|
||||||
tc.client.Append("inbox", nil, &received, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
uid1 := imapclient.FetchUID(1)
|
uid1 := imapclient.FetchUID(1)
|
||||||
@ -288,7 +288,7 @@ func TestFetch(t *testing.T) {
|
|||||||
Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"boundary", "unique-boundary-1"}}},
|
Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"boundary", "unique-boundary-1"}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
tc.client.Append("inbox", nil, &received, []byte(nestedMessage))
|
tc.client.Append("inbox", makeAppendTime(nestedMessage, received))
|
||||||
tc.transactf("ok", "fetch 2 bodystructure")
|
tc.transactf("ok", "fetch 2 bodystructure")
|
||||||
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
||||||
|
|
||||||
|
@ -31,12 +31,12 @@ func TestMove(t *testing.T) {
|
|||||||
tc.transactf("bad", "move 1 inbox ") // Leftover.
|
tc.transactf("bad", "move 1 inbox ") // Leftover.
|
||||||
|
|
||||||
// Seqs 1,2 and UIDs 3,4.
|
// Seqs 1,2 and UIDs 3,4.
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.StoreFlagsSet("1:2", true, `\Deleted`)
|
tc.client.StoreFlagsSet("1:2", true, `\Deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
|
|
||||||
tc.client.Unselect()
|
tc.client.Unselect()
|
||||||
tc.client.Examine("inbox")
|
tc.client.Examine("inbox")
|
||||||
@ -69,8 +69,8 @@ func TestMove(t *testing.T) {
|
|||||||
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
tc3.xuntagged(imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1))
|
||||||
|
|
||||||
// UIDs 5,6
|
// UIDs 5,6
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc2.transactf("ok", "noop") // Drain.
|
tc2.transactf("ok", "noop") // Drain.
|
||||||
tc3.transactf("ok", "noop") // Drain.
|
tc3.transactf("ok", "noop") // Drain.
|
||||||
|
|
||||||
|
@ -67,13 +67,13 @@ func TestSearch(t *testing.T) {
|
|||||||
received := time.Date(2020, time.January, 1, 10, 0, 0, 0, time.UTC)
|
received := time.Date(2020, time.January, 1, 10, 0, 0, 0, time.UTC)
|
||||||
saveDate := time.Now()
|
saveDate := time.Now()
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
tc.client.Append("inbox", nil, &received, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
||||||
}
|
}
|
||||||
tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
|
tc.client.StoreFlagsSet("1:4", true, `\Deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
|
|
||||||
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
||||||
tc.client.Append("inbox", nil, &received, []byte(searchMsg))
|
tc.client.Append("inbox", makeAppendTime(searchMsg, received))
|
||||||
|
|
||||||
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
received = time.Date(2022, time.January, 1, 9, 0, 0, 0, time.UTC)
|
||||||
mostFlags := []string{
|
mostFlags := []string{
|
||||||
@ -90,7 +90,7 @@ func TestSearch(t *testing.T) {
|
|||||||
`custom1`,
|
`custom1`,
|
||||||
`Custom2`,
|
`Custom2`,
|
||||||
}
|
}
|
||||||
tc.client.Append("inbox", mostFlags, &received, []byte(searchMsg))
|
tc.client.Append("inbox", imapclient.Append{Flags: mostFlags, Received: &received, Size: int64(len(searchMsg)), Data: strings.NewReader(searchMsg)})
|
||||||
|
|
||||||
// We now have sequence numbers 1,2,3 and UIDs 5,6,7.
|
// We now have sequence numbers 1,2,3 and UIDs 5,6,7.
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ func testSelectExamine(t *testing.T, examine bool) {
|
|||||||
tc.xcode(okcode)
|
tc.xcode(okcode)
|
||||||
|
|
||||||
// Append a message. It will be reported as UNSEEN.
|
// Append a message. It will be reported as UNSEEN.
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.transactf("ok", "%s inbox", cmd)
|
tc.transactf("ok", "%s inbox", cmd)
|
||||||
tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist)
|
tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist)
|
||||||
tc.xcode(okcode)
|
tc.xcode(okcode)
|
||||||
|
@ -2340,7 +2340,7 @@ func (c *conn) cmdLogin(tag, cmd string, p *parser) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if account != nil {
|
if account != nil {
|
||||||
err := account.Close()
|
err := account.Close()
|
||||||
c.log.Check(err, "close account")
|
c.xsanity(err, "close account")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -2886,7 +2886,7 @@ func (c *conn) cmdDelete(tag, cmd string, p *parser) {
|
|||||||
for _, mID := range removeMessageIDs {
|
for _, mID := range removeMessageIDs {
|
||||||
p := c.account.MessagePath(mID)
|
p := c.account.MessagePath(mID)
|
||||||
err := os.Remove(p)
|
err := os.Remove(p)
|
||||||
c.log.Check(err, "removing message file for mailbox delete", slog.String("path", p))
|
c.xsanity(err, "removing message file %q for mailbox delete", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.ok(tag, cmd)
|
c.ok(tag, cmd)
|
||||||
@ -3562,7 +3562,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
|||||||
for _, a := range appends {
|
for _, a := range appends {
|
||||||
c.uidAppend(a.m.UID)
|
c.uidAppend(a.m.UID)
|
||||||
}
|
}
|
||||||
// todo spec: with condstore/qresync, is there a mechanism to the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed.
|
// todo spec: with condstore/qresync, is there a mechanism to let the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed.
|
||||||
c.bwritelinef("* %d EXISTS", len(c.uids))
|
c.bwritelinef("* %d EXISTS", len(c.uids))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4382,7 +4382,7 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.enabled[capQresync] {
|
if qresync {
|
||||||
// ../rfc/9051:6744 ../rfc/7162:1334
|
// ../rfc/9051:6744 ../rfc/7162:1334
|
||||||
c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
|
c.writeresultf("%s OK [HIGHESTMODSEQ %d] move", tag, modseq.Client())
|
||||||
} else {
|
} else {
|
||||||
|
@ -341,6 +341,14 @@ func xparseUIDRange(s string) imapclient.NumRange {
|
|||||||
return nr
|
return nr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeAppend(msg string) imapclient.Append {
|
||||||
|
return imapclient.Append{Size: int64(len(msg)), Data: strings.NewReader(msg)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAppendTime(msg string, tm time.Time) imapclient.Append {
|
||||||
|
return imapclient.Append{Received: &tm, Size: int64(len(msg)), Data: strings.NewReader(msg)}
|
||||||
|
}
|
||||||
|
|
||||||
var connCounter int64
|
var connCounter int64
|
||||||
|
|
||||||
func start(t *testing.T) *testconn {
|
func start(t *testing.T) *testconn {
|
||||||
@ -732,8 +740,8 @@ func TestSequence(t *testing.T) {
|
|||||||
tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
|
tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
|
||||||
tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
|
tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
|
||||||
|
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
|
tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
|
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
|
||||||
@ -753,7 +761,7 @@ func DisabledTestReference(t *testing.T) {
|
|||||||
defer tc.close()
|
defer tc.close()
|
||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.client.Login("mjl@mox.example", password0)
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t)
|
tc2 := startNoSwitchboard(t)
|
||||||
defer tc2.close()
|
defer tc2.close()
|
||||||
|
@ -14,7 +14,7 @@ func TestStore(t *testing.T) {
|
|||||||
tc.client.Login("mjl@mox.example", password0)
|
tc.client.Login("mjl@mox.example", password0)
|
||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable("imap4rev2")
|
||||||
|
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
uid1 := imapclient.FetchUID(1)
|
uid1 := imapclient.FetchUID(1)
|
||||||
|
@ -18,7 +18,7 @@ func TestUnselect(t *testing.T) {
|
|||||||
tc.transactf("no", "fetch 1 all") // Invalid when not selected.
|
tc.transactf("no", "fetch 1 all") // Invalid when not selected.
|
||||||
|
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.StoreFlagsAdd("1", true, `\Deleted`)
|
tc.client.StoreFlagsAdd("1", true, `\Deleted`)
|
||||||
tc.transactf("ok", "unselect")
|
tc.transactf("ok", "unselect")
|
||||||
tc.transactf("ok", "status inbox (messages)")
|
tc.transactf("ok", "status inbox (messages)")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user