mox/imapserver/fetch_test.go
Mechiel Lukkien e7b562e3f2
imapclient: first step towards making package usable as imap client with other imap servers, and minor imapserver bug fix
The imapclient needs more changes, like more strict parsing, before it can be a
generally usable IMAP client, these are a few steps towards that.

- Fix a bug in the imapserver METADATA responses for TOOMANY and MAXSIZE.
- Split low-level IMAP protocol handling (new Proto type) from the higher-level
  client command handling (existing Conn type). The idea is that some simple
  uses of IMAP can get by with just using these commands, while more intricate
  uses of IMAP (like a synchronizing client that needs to talk to all kinds of
  servers with different behaviours and implemented extensions) can write custom
  commands and read untagged responses or command completion results
  explicitly. The lower-level method names have clearer names now, like
  ReadResponse instead of Response.
- Merge the untagged responses and (command completion) "Result" into a new
  type Response. Makes function signatures simpler. And make Response implement
  the error interface, and change command methods to return the Response as error
  if the result is NO or BAD. Simplifies error handling, and still provides the
  option to continue after a NO or BAD.
- Add UIDSearch/MSNSearch commands, with a custom "search program", so mostly
  to indicate these commands exist.
- More complete coverage of types for response codes, for easier handling.
- Automatically handle any ENABLED or CAPABILITY untagged response or response
  code for IMAP command methods on type Conn.
- Make difference between MSN vs UID versions of
  FETCH/STORE/SEARCH/COPY/MOVE/REPLACE commands more clear. The original MSN
  commands now have MSN prefixed to their name, so they are grouped together in
  the documentation.
- Document which capabilities are needed for a command.
2025-04-15 08:37:18 +02:00

565 lines
23 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(imapclient.CapIMAP4rev2)
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"
bodyext1 := imapclient.BodyExtension1Part{
Disposition: ptr((*string)(nil)),
DispositionParams: ptr([][2]string(nil)),
Language: ptr([]string(nil)),
Location: ptr((*string)(nil)),
}
bodystructbody1.Ext = &bodyext1
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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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.MSNStoreFlagsClear("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: &bodyext1},
imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "US-ASCII"}}, Octets: 114}, Lines: 3, Ext: &bodyext1},
imapclient.BodyTypeMpart{
Bodies: []any{
imapclient.BodyTypeBasic{MediaType: "AUDIO", MediaSubtype: "BASIC", BodyFields: imapclient.BodyFields{CTE: "BASE64", Octets: 22}, Ext: &bodyext1},
imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}, Ext: &imapclient.BodyExtension1Part{
Disposition: ptr(ptr("inline")),
DispositionParams: ptr([][2]string{{"filename", "image.jpg"}}),
Language: ptr([]string(nil)),
Location: ptr((*string)(nil)),
}},
},
MediaSubtype: "PARALLEL",
Ext: &imapclient.BodyExtensionMpart{
Params: [][2]string{{"BOUNDARY", "unique-boundary-2"}},
Disposition: ptr((*string)(nil)), // Present but nil.
DispositionParams: ptr([][2]string(nil)),
Language: ptr([]string(nil)),
Location: ptr((*string)(nil)),
},
},
imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5, Ext: &bodyext1},
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: &bodyext1},
Lines: 7,
Ext: &imapclient.BodyExtension1Part{
MD5: ptr("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="),
Disposition: ptr((*string)(nil)),
DispositionParams: ptr([][2]string(nil)),
Language: ptr([]string{"en", "de"}),
Location: ptr(ptr("http://localhost")),
},
},
},
MediaSubtype: "MIXED",
Ext: &imapclient.BodyExtensionMpart{
Params: [][2]string{{"BOUNDARY", "unique-boundary-1"}},
Disposition: ptr((*string)(nil)), // Present but nil.
DispositionParams: ptr([][2]string(nil)),
Language: ptr([]string(nil)),
Location: ptr((*string)(nil)),
},
},
}
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()
}