mirror of
https://github.com/mjl-/mox.git
synced 2025-06-27 23:08:14 +03:00

Once clients enable this extension, commands can no longer refer to "message sequence numbers" (MSNs), but can only refer to messages with UIDs. This means both sides no longer have to carefully keep their sequence numbers in sync (error-prone), and don't have to keep track of a mapping of sequence numbers to UIDs (saves resources). With UIDONLY enabled, all FETCH responses are replaced with UIDFETCH response.
540 lines
22 KiB
Go
540 lines
22 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mjl-/bstore"
|
|
|
|
"github.com/mjl-/mox/imapclient"
|
|
"github.com/mjl-/mox/store"
|
|
)
|
|
|
|
func TestFetch(t *testing.T) {
|
|
testFetch(t, false)
|
|
}
|
|
|
|
func TestFetchUIDOnly(t *testing.T) {
|
|
testFetch(t, true)
|
|
}
|
|
|
|
func testFetch(t *testing.T, uidonly bool) {
|
|
tc := start(t, uidonly)
|
|
defer tc.close()
|
|
|
|
tc.login("mjl@mox.example", password0)
|
|
tc.client.Enable("imap4rev2")
|
|
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
|
tc.check(err, "parse time")
|
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
|
tc.client.Select("inbox")
|
|
|
|
uid1 := imapclient.FetchUID(1)
|
|
date1 := imapclient.FetchInternalDate{Date: received}
|
|
rfcsize1 := imapclient.FetchRFC822Size(len(exampleMsg))
|
|
env1 := imapclient.FetchEnvelope{
|
|
Date: "Mon, 7 Feb 1994 21:52:25 -0800",
|
|
Subject: "afternoon meeting",
|
|
From: []imapclient.Address{{Name: "Fred Foobar", Mailbox: "foobar", Host: "blurdybloop.example"}},
|
|
Sender: []imapclient.Address{{Name: "Fred Foobar", Mailbox: "foobar", Host: "blurdybloop.example"}},
|
|
ReplyTo: []imapclient.Address{{Name: "Fred Foobar", Mailbox: "foobar", Host: "blurdybloop.example"}},
|
|
To: []imapclient.Address{{Mailbox: "mooch", Host: "owatagu.siam.edu.example"}},
|
|
MessageID: "<B27397-0100000@Blurdybloop.example>",
|
|
}
|
|
noflags := imapclient.FetchFlags(nil)
|
|
bodystructbody1 := imapclient.BodyTypeText{
|
|
MediaType: "TEXT",
|
|
MediaSubtype: "PLAIN",
|
|
BodyFields: imapclient.BodyFields{
|
|
Params: [][2]string{[...]string{"CHARSET", "US-ASCII"}},
|
|
Octets: 57,
|
|
},
|
|
Lines: 2,
|
|
}
|
|
bodyxstructure1 := imapclient.FetchBodystructure{
|
|
RespAttr: "BODY",
|
|
Body: bodystructbody1,
|
|
}
|
|
bodystructure1 := bodyxstructure1
|
|
bodystructure1.RespAttr = "BODYSTRUCTURE"
|
|
bodystructbody1.Ext = &imapclient.BodyExtension1Part{}
|
|
bodystructure1.Body = bodystructbody1
|
|
|
|
split := strings.SplitN(exampleMsg, "\r\n\r\n", 2)
|
|
exampleMsgHeader := split[0] + "\r\n\r\n"
|
|
exampleMsgBody := split[1]
|
|
|
|
binary1 := imapclient.FetchBinary{RespAttr: "BINARY[]", Data: exampleMsg}
|
|
binarypart1 := imapclient.FetchBinary{RespAttr: "BINARY[1]", Parts: []uint32{1}, Data: exampleMsgBody}
|
|
binarypartial1 := imapclient.FetchBinary{RespAttr: "BINARY[]", Data: exampleMsg[1:2]}
|
|
binarypartpartial1 := imapclient.FetchBinary{RespAttr: "BINARY[1]", Parts: []uint32{1}, Data: exampleMsgBody[1:2]}
|
|
binaryend1 := imapclient.FetchBinary{RespAttr: "BINARY[]", Data: ""}
|
|
binarypartend1 := imapclient.FetchBinary{RespAttr: "BINARY[1]", Parts: []uint32{1}, Data: ""}
|
|
binarysize1 := imapclient.FetchBinarySize{RespAttr: "BINARY.SIZE[]", Size: int64(len(exampleMsg))}
|
|
binarysizepart1 := imapclient.FetchBinarySize{RespAttr: "BINARY.SIZE[1]", Parts: []uint32{1}, Size: int64(len(exampleMsgBody))}
|
|
bodyheader1 := imapclient.FetchBody{RespAttr: "BODY[HEADER]", Section: "HEADER", Body: exampleMsgHeader}
|
|
bodytext1 := imapclient.FetchBody{RespAttr: "BODY[TEXT]", Section: "TEXT", Body: exampleMsgBody}
|
|
body1 := imapclient.FetchBody{RespAttr: "BODY[]", Body: exampleMsg}
|
|
bodypart1 := imapclient.FetchBody{RespAttr: "BODY[1]", Section: "1", Body: exampleMsgBody}
|
|
bodyoff1 := imapclient.FetchBody{RespAttr: "BODY[]<1>", Section: "", Offset: 1, Body: exampleMsg[1:3]}
|
|
body1off1 := imapclient.FetchBody{RespAttr: "BODY[1]<1>", Section: "1", Offset: 1, Body: exampleMsgBody[1:3]}
|
|
bodyend1 := imapclient.FetchBody{RespAttr: "BODY[1]<100000>", Section: "1", Offset: 100000, Body: ""} // todo: should offset be what was requested, or the size of the message?
|
|
rfcheader1 := imapclient.FetchRFC822Header(exampleMsgHeader)
|
|
rfctext1 := imapclient.FetchRFC822Text(exampleMsgBody)
|
|
rfc1 := imapclient.FetchRFC822(exampleMsg)
|
|
headerSplit := strings.SplitN(exampleMsgHeader, "\r\n", 2)
|
|
dateheader1 := imapclient.FetchBody{RespAttr: "BODY[HEADER.FIELDS (Date)]", Section: "HEADER.FIELDS (Date)", Body: headerSplit[0] + "\r\n\r\n"}
|
|
nodateheader1 := imapclient.FetchBody{RespAttr: "BODY[HEADER.FIELDS.NOT (Date)]", Section: "HEADER.FIELDS.NOT (Date)", Body: headerSplit[1]}
|
|
mime1 := imapclient.FetchBody{RespAttr: "BODY[1.MIME]", Section: "1.MIME", Body: "Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n"}
|
|
|
|
flagsSeen := imapclient.FetchFlags{`\Seen`}
|
|
|
|
if !uidonly {
|
|
tc.transactf("ok", "fetch 1 all")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, env1, noflags))
|
|
|
|
tc.transactf("ok", "fetch 1 fast")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, noflags))
|
|
|
|
tc.transactf("ok", "fetch 1 full")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, date1, rfcsize1, env1, bodyxstructure1, noflags))
|
|
|
|
tc.transactf("ok", "fetch 1 flags")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags))
|
|
|
|
tc.transactf("ok", "fetch 1 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1))
|
|
|
|
// Should be returned unmodified, because there is no content-transfer-encoding.
|
|
tc.transactf("ok", "fetch 1 binary[]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, binary1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
tc.transactf("ok", "fetch 1 binary[1]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarypart1)) // Seen flag not changed.
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
|
|
tc.xuntagged(
|
|
tc.untaggedFetch(1, 1, binarypartial1, noflags),
|
|
tc.untaggedFetch(1, 1, flagsSeen), // For UID FETCH, we get the flags during the command.
|
|
)
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 binary[1]<1.1>")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartpartial1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 binary[]<10000.10001>")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, binaryend1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 binary[1]<10000.10001>")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartend1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
tc.transactf("ok", "fetch 1 binary.size[]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarysize1))
|
|
|
|
tc.transactf("ok", "fetch 1 binary.size[1]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarysizepart1))
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
tc.transactf("ok", "fetch 1 body[]<1.2>")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodyoff1)) // Already seen.
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged() // Already seen.
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[1]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodypart1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[1]<1.2>")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1off1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[1]<100000.100000>")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodyend1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[header]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodyheader1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[text]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodytext1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
// equivalent to body.peek[header], ../rfc/3501:3183
|
|
tc.transactf("ok", "fetch 1 rfc822.header")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfcheader1))
|
|
|
|
// equivalent to body[text], ../rfc/3501:3199
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 rfc822.text")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
// equivalent to body[], ../rfc/3501:3179
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 rfc822")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfc1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
|
|
// With PEEK, we should not get the \Seen flag.
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body.peek[]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
|
|
|
tc.transactf("ok", "fetch 1 binary.peek[]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, binary1))
|
|
|
|
// HEADER.FIELDS and .NOT
|
|
tc.transactf("ok", "fetch 1 body.peek[header.fields (date)]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, dateheader1))
|
|
tc.transactf("ok", "fetch 1 body.peek[header.fields.not (date)]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, nodateheader1))
|
|
// For non-multipart messages, 1 means the whole message, but since it's not of
|
|
// type message/{rfc822,global} (a message), you can't get the message headers.
|
|
// ../rfc/9051:4481
|
|
tc.transactf("no", "fetch 1 body.peek[1.header]")
|
|
|
|
// MIME, part 1 for non-multipart messages is the message itself. ../rfc/9051:4481
|
|
tc.transactf("ok", "fetch 1 body.peek[1.mime]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, mime1))
|
|
|
|
// Missing sequence number. ../rfc/9051:7018
|
|
tc.transactf("bad", "fetch 2 body[]")
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1:1 body[]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
|
} else {
|
|
tc.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
|
tc.transactf("ok", "noop")
|
|
}
|
|
|
|
// UID fetch
|
|
tc.transactf("ok", "uid fetch 1 body[]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
|
|
|
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(tc.untaggedFetch(1, 1, imapclient.FetchSaveDate{SaveDate: &saveDate}))
|
|
|
|
// Test some invalid syntax. Also invalid for uidonly.
|
|
tc.transactf("bad", "fetch")
|
|
tc.transactf("bad", "fetch ")
|
|
tc.transactf("bad", "fetch ")
|
|
tc.transactf("bad", "fetch 1") // At least one requested item required.
|
|
tc.transactf("bad", "fetch 1 ()") // Empty list not allowed
|
|
tc.transactf("bad", "fetch 1 unknown")
|
|
tc.transactf("bad", "fetch 1 (unknown)")
|
|
tc.transactf("bad", "fetch 1 (all)") // Macro's not allowed in list.
|
|
tc.transactf("bad", "fetch 1 binary") // [] required
|
|
tc.transactf("bad", "fetch 1 binary[text]") // Text/header etc only allowed for body[].
|
|
tc.transactf("bad", "fetch 1 binary[]<1>") // Count required.
|
|
tc.transactf("bad", "fetch 1 binary[]<1.0>") // Count must be > 0.
|
|
tc.transactf("bad", "fetch 1 binary[]<1..1>") // Single dot.
|
|
tc.transactf("bad", "fetch 1 body[]<1>") // Count required.
|
|
tc.transactf("bad", "fetch 1 body[]<1.0>") // Count must be > 0.
|
|
tc.transactf("bad", "fetch 1 body[]<1..1>") // Single dot.
|
|
tc.transactf("bad", "fetch 1 body[header.fields]") // List of headers required.
|
|
tc.transactf("bad", "fetch 1 body[header.fields ()]") // List must be non-empty.
|
|
tc.transactf("bad", "fetch 1 body[header.fields.not]") // List of headers required.
|
|
tc.transactf("bad", "fetch 1 body[header.fields.not ()]") // List must be non-empty.
|
|
tc.transactf("bad", "fetch 1 body[mime]") // MIME must be prefixed with a number. ../rfc/9051:4497
|
|
|
|
if !uidonly {
|
|
tc.transactf("no", "fetch 1 body[2]") // No such part.
|
|
}
|
|
|
|
// Add more complex message.
|
|
|
|
bodystructure2 := imapclient.FetchBodystructure{
|
|
RespAttr: "BODYSTRUCTURE",
|
|
Body: imapclient.BodyTypeMpart{
|
|
Bodies: []any{
|
|
imapclient.BodyTypeBasic{BodyFields: imapclient.BodyFields{Octets: 275}, Ext: &imapclient.BodyExtension1Part{}},
|
|
imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "US-ASCII"}}, Octets: 114}, Lines: 3, Ext: &imapclient.BodyExtension1Part{}},
|
|
imapclient.BodyTypeMpart{
|
|
Bodies: []any{
|
|
imapclient.BodyTypeBasic{MediaType: "AUDIO", MediaSubtype: "BASIC", BodyFields: imapclient.BodyFields{CTE: "BASE64", Octets: 22}, Ext: &imapclient.BodyExtension1Part{}},
|
|
imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}, Ext: &imapclient.BodyExtension1Part{Disposition: "inline", DispositionParams: [][2]string{{"filename", "image.jpg"}}}},
|
|
},
|
|
MediaSubtype: "PARALLEL",
|
|
Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"BOUNDARY", "unique-boundary-2"}}},
|
|
},
|
|
imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5, Ext: &imapclient.BodyExtension1Part{}},
|
|
imapclient.BodyTypeMsg{
|
|
MediaType: "MESSAGE",
|
|
MediaSubtype: "RFC822",
|
|
BodyFields: imapclient.BodyFields{Octets: 228},
|
|
Envelope: imapclient.Envelope{
|
|
Subject: "(subject in US-ASCII)",
|
|
From: []imapclient.Address{{Name: "", Adl: "", Mailbox: "info", Host: "mox.example"}},
|
|
Sender: []imapclient.Address{{Name: "", Adl: "", Mailbox: "info", Host: "mox.example"}},
|
|
ReplyTo: []imapclient.Address{{Name: "", Adl: "", Mailbox: "info", Host: "mox.example"}},
|
|
To: []imapclient.Address{{Name: "mox", Adl: "", Mailbox: "info", Host: "mox.example"}},
|
|
},
|
|
Bodystructure: imapclient.BodyTypeText{
|
|
MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "ISO-8859-1"}}, CTE: "QUOTED-PRINTABLE", Octets: 51}, Lines: 1, Ext: &imapclient.BodyExtension1Part{}},
|
|
Lines: 7,
|
|
Ext: &imapclient.BodyExtension1Part{
|
|
MD5: "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
|
|
Language: []string{"en", "de"},
|
|
Location: "http://localhost",
|
|
},
|
|
},
|
|
},
|
|
MediaSubtype: "MIXED",
|
|
Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"BOUNDARY", "unique-boundary-1"}}},
|
|
},
|
|
}
|
|
tc.client.Append("inbox", makeAppendTime(nestedMessage, received))
|
|
tc.transactf("ok", "uid fetch 2 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
|
|
|
// Multiple responses.
|
|
if !uidonly {
|
|
tc.transactf("ok", "fetch 1:2 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
|
tc.transactf("ok", "fetch 1,2 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
|
tc.transactf("ok", "fetch 2:1 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
|
tc.transactf("ok", "fetch 1:* bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
|
tc.transactf("ok", "fetch *:1 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
|
tc.transactf("ok", "fetch *:2 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
|
tc.transactf("ok", "fetch * bodystructure") // Highest msgseq.
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
|
}
|
|
|
|
tc.transactf("ok", "uid fetch 1:* bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
|
|
|
tc.transactf("ok", "uid fetch 1:2 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
|
|
|
tc.transactf("ok", "uid fetch 1,2 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodystructure1), tc.untaggedFetch(2, 2, bodystructure2))
|
|
|
|
tc.transactf("ok", "uid fetch 2:2 bodystructure")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, bodystructure2))
|
|
|
|
// todo: read the bodies/headers of the parts, and of the nested message.
|
|
tc.transactf("ok", "uid fetch 2 body.peek[]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[]", Body: nestedMessage}))
|
|
|
|
part1 := tocrlf(` ... Some text appears here ...
|
|
|
|
[Note that the blank between the boundary and the start
|
|
of the text in this part means no header fields were
|
|
given and this is text in the US-ASCII character set.
|
|
It could have been done with explicit typing as in the
|
|
next part.]
|
|
`)
|
|
tc.transactf("ok", "uid fetch 2 body.peek[1]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[1]", Section: "1", Body: part1}))
|
|
|
|
tc.transactf("no", "uid fetch 2 binary.peek[3]") // Only allowed on leaf parts, not multiparts.
|
|
tc.transactf("no", "uid fetch 2 binary.peek[5]") // Only allowed on leaf parts, not messages.
|
|
|
|
part31 := "aGVsbG8NCndvcmxkDQo=\r\n"
|
|
part31dec := "hello\r\nworld\r\n"
|
|
tc.transactf("ok", "uid fetch 2 binary.size[3.1]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBinarySize{RespAttr: "BINARY.SIZE[3.1]", Parts: []uint32{3, 1}, Size: int64(len(part31dec))}))
|
|
|
|
tc.transactf("ok", "uid fetch 2 body.peek[3.1]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[3.1]", Section: "3.1", Body: part31}))
|
|
|
|
tc.transactf("ok", "uid fetch 2 binary.peek[3.1]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBinary{RespAttr: "BINARY[3.1]", Parts: []uint32{3, 1}, Data: part31dec}))
|
|
|
|
part3 := tocrlf(`--unique-boundary-2
|
|
Content-Type: audio/basic
|
|
Content-Transfer-Encoding: base64
|
|
|
|
aGVsbG8NCndvcmxkDQo=
|
|
|
|
--unique-boundary-2
|
|
Content-Type: image/jpeg
|
|
Content-Transfer-Encoding: base64
|
|
Content-Disposition: inline; filename=image.jpg
|
|
|
|
|
|
--unique-boundary-2--
|
|
|
|
`)
|
|
tc.transactf("ok", "uid fetch 2 body.peek[3]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[3]", Section: "3", Body: part3}))
|
|
|
|
part2mime := "Content-type: text/plain; charset=US-ASCII\r\n"
|
|
tc.transactf("ok", "uid fetch 2 body.peek[2.mime]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[2.MIME]", Section: "2.MIME", Body: part2mime}))
|
|
|
|
part5 := tocrlf(`From: info@mox.example
|
|
To: mox <info@mox.example>
|
|
Subject: (subject in US-ASCII)
|
|
Content-Type: Text/plain; charset=ISO-8859-1
|
|
Content-Transfer-Encoding: Quoted-printable
|
|
|
|
... Additional text in ISO-8859-1 goes here ...
|
|
`)
|
|
tc.transactf("ok", "uid fetch 2 body.peek[5]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5]", Section: "5", Body: part5}))
|
|
|
|
part5header := tocrlf(`From: info@mox.example
|
|
To: mox <info@mox.example>
|
|
Subject: (subject in US-ASCII)
|
|
Content-Type: Text/plain; charset=ISO-8859-1
|
|
Content-Transfer-Encoding: Quoted-printable
|
|
|
|
`)
|
|
tc.transactf("ok", "uid fetch 2 body.peek[5.header]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5.HEADER]", Section: "5.HEADER", Body: part5header}))
|
|
|
|
part5mime := tocrlf(`Content-Type: message/rfc822
|
|
Content-MD5: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
|
|
Content-Language: en,de
|
|
Content-Location: http://localhost
|
|
`)
|
|
tc.transactf("ok", "uid fetch 2 body.peek[5.mime]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5.MIME]", Section: "5.MIME", Body: part5mime}))
|
|
|
|
part5text := " ... Additional text in ISO-8859-1 goes here ...\r\n"
|
|
|
|
tc.transactf("ok", "uid fetch 2 body.peek[5.text]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5.TEXT]", Section: "5.TEXT", Body: part5text}))
|
|
|
|
part5body := " ... Additional text in ISO-8859-1 goes here ...\r\n"
|
|
tc.transactf("ok", "uid fetch 2 body.peek[5.1]")
|
|
tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchBody{RespAttr: "BODY[5.1]", Section: "5.1", Body: part5body}))
|
|
|
|
// 5.1 is the part that is the sub message, but not as message/rfc822, but as part,
|
|
// so we cannot request a header.
|
|
tc.transactf("no", "uid fetch 2 body.peek[5.1.header]")
|
|
|
|
// In case of EXAMINE instead of SELECT, we should not be seeing any changed \Seen flags for non-peek commands.
|
|
tc.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
|
tc.client.Unselect()
|
|
tc.client.Examine("inbox")
|
|
|
|
// Preview
|
|
preview := "Hello Joe, do you think we can meet at 3:30 tomorrow?"
|
|
tc.transactf("ok", "uid fetch 1 preview")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
|
|
|
|
tc.transactf("ok", "uid fetch 1 preview (lazy)")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
|
|
|
|
// On-demand preview and saving on first request.
|
|
err = tc.account.DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
|
m := store.Message{ID: 1}
|
|
err := tx.Get(&m)
|
|
tcheck(t, err, "get message")
|
|
if m.UID != 1 {
|
|
t.Fatalf("uid %d instead of 1", m.UID)
|
|
}
|
|
m.Preview = nil
|
|
err = tx.Update(&m)
|
|
tcheck(t, err, "remove preview from message")
|
|
return nil
|
|
})
|
|
tcheck(t, err, "remove preview from database")
|
|
|
|
tc.transactf("ok", "uid fetch 1 preview")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchPreview{Preview: &preview}))
|
|
m := store.Message{ID: 1}
|
|
err = tc.account.DB.Get(ctxbg, &m)
|
|
tcheck(t, err, "get message")
|
|
if m.Preview == nil {
|
|
t.Fatalf("preview missing")
|
|
} else if *m.Preview != preview+"\n" {
|
|
t.Fatalf("got preview %q, expected %q", *m.Preview, preview+"\n")
|
|
}
|
|
|
|
tc.transactf("bad", "uid fetch 1 preview (bogus)")
|
|
|
|
// Start a second session. Use it to remove the message. First session should still
|
|
// be able to access the messages.
|
|
tc2 := startNoSwitchboard(t, uidonly)
|
|
defer tc2.closeNoWait()
|
|
tc2.login("mjl@mox.example", password0)
|
|
tc2.client.Select("inbox")
|
|
tc2.client.UIDStoreFlagsSet("1", true, `\Deleted`)
|
|
tc2.client.Expunge()
|
|
tc2.client.Logout()
|
|
|
|
if uidonly {
|
|
tc.transactf("ok", "uid fetch 1 binary[]")
|
|
tc.xuntagged(
|
|
tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`}),
|
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("1")},
|
|
)
|
|
// Message no longer available in session.
|
|
} else {
|
|
tc.transactf("ok", "fetch 1 binary[]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, binary1))
|
|
|
|
tc.transactf("ok", "fetch 1 body[]")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
|
|
|
tc.transactf("ok", "fetch 1 rfc822.text")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1))
|
|
|
|
tc.transactf("ok", "fetch 1 rfc822")
|
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfc1))
|
|
}
|
|
|
|
tc.client.Logout()
|
|
}
|