mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 01:48:15 +03:00

The gmail iOS/Android app were showing mime image parts as (garbled) text instead of rendering them as image. By returning all the optional fields in the bodystructure fetch attribute, the gmail app renders the image as expected by the user. So we now add all fields. We didn't before, because we weren't keeping track of Content-MD5, Content-Language and Content-Location header fields, since they aren't that useful. Messages in mailboxes have to be reparsed: ./mox reparse Without reparsing, imap responses will claim the extra fields (content-disposition) are absent for existing messages, instead of not claiming anything at all, which is what we did before. Accounts and all/some mailboxes can get their "uid validity" bumped ("./mox bumpuidvalidity $account [$mailbox]"), which should trigger clients to load all messages from scratch, but gmail doesn't appear to notice, so it would be better to remove & add the account in gmail. For issue #327, also relevant to issue #217.
516 lines
26 KiB
Go
516 lines
26 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) {
|
|
tc := start(t)
|
|
defer tc.close()
|
|
|
|
tc.client.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]}
|
|
date1header1 := imapclient.FetchBody{RespAttr: "BODY[1.HEADER.FIELDS (Date)]", Section: "1.HEADER.FIELDS (Date)", Body: headerSplit[0] + "\r\n\r\n"}
|
|
nodate1header1 := imapclient.FetchBody{RespAttr: "BODY[1.HEADER.FIELDS.NOT (Date)]", Section: "1.HEADER.FIELDS.NOT (Date)", Body: headerSplit[1]}
|
|
mime1 := imapclient.FetchBody{RespAttr: "BODY[1.MIME]", Section: "1.MIME", Body: "MIME-Version: 1.0\r\nContent-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n\r\n"}
|
|
|
|
flagsSeen := imapclient.FetchFlags{`\Seen`}
|
|
|
|
tc.transactf("ok", "fetch 1 all")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, env1, noflags}})
|
|
|
|
tc.transactf("ok", "fetch 1 fast")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, noflags}})
|
|
|
|
tc.transactf("ok", "fetch 1 full")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1, rfcsize1, env1, bodyxstructure1, noflags}})
|
|
|
|
tc.transactf("ok", "fetch 1 flags")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, noflags}})
|
|
|
|
tc.transactf("ok", "fetch 1 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}})
|
|
|
|
// Should be returned unmodified, because there is no content-transfer-encoding.
|
|
tc.transactf("ok", "fetch 1 binary[]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
tc.transactf("ok", "fetch 1 binary[1]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypart1}}) // Seen flag not changed.
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartial1, noflags}},
|
|
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, 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(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartpartial1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 binary[]<10000.10001>")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binaryend1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 binary[1]<10000.10001>")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarypartend1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 binary.size[]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarysize1}})
|
|
|
|
tc.transactf("ok", "fetch 1 binary.size[1]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binarysizepart1}})
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
tc.transactf("ok", "fetch 1 body[]<1.2>")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, 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(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodypart1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[1]<1.2>")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1off1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[1]<100000.100000>")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyend1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[header]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodyheader1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 body[text]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodytext1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
// equivalent to body.peek[header], ../rfc/3501:3183
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 rfc822.header")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfcheader1}})
|
|
|
|
// equivalent to body[text], ../rfc/3501:3199
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 rfc822.text")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfctext1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
// equivalent to body[], ../rfc/3501:3179
|
|
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
|
tc.transactf("ok", "fetch 1 rfc822")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfc1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, 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(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
|
|
|
|
tc.transactf("ok", "fetch 1 binary.peek[]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1}})
|
|
|
|
// HEADER.FIELDS and .NOT
|
|
tc.transactf("ok", "fetch 1 body.peek[header.fields (date)]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, dateheader1}})
|
|
tc.transactf("ok", "fetch 1 body.peek[header.fields.not (date)]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, nodateheader1}})
|
|
// For non-multipart messages, 1 means the whole message. ../rfc/9051:4481
|
|
tc.transactf("ok", "fetch 1 body.peek[1.header.fields (date)]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, date1header1}})
|
|
tc.transactf("ok", "fetch 1 body.peek[1.header.fields.not (date)]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, nodate1header1}})
|
|
|
|
// 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(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, mime1}})
|
|
|
|
// Missing sequence number. ../rfc/9051:7018
|
|
tc.transactf("bad", "fetch 2 body[]")
|
|
|
|
tc.transactf("ok", "fetch 1:1 body[]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1, noflags}})
|
|
tc.transactf("ok", "noop")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flagsSeen}})
|
|
|
|
// UID fetch
|
|
tc.transactf("ok", "uid fetch 1 body[]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
|
|
|
|
// UID fetch
|
|
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 ")
|
|
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
|
|
|
|
tc.transactf("no", "fetch 1 body[2]") // No such part.
|
|
|
|
// Add more complex message.
|
|
|
|
uid2 := imapclient.FetchUID(2)
|
|
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", "fetch 2 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
|
|
// Multiple responses.
|
|
tc.transactf("ok", "fetch 1:2 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
tc.transactf("ok", "fetch 1,2 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
tc.transactf("ok", "fetch 2:1 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
tc.transactf("ok", "fetch 1:* bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
tc.transactf("ok", "fetch *:1 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
tc.transactf("ok", "fetch *:2 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
|
|
tc.transactf("ok", "fetch * bodystructure") // Highest msgseq.
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
|
|
tc.transactf("ok", "uid fetch 1:* bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
|
|
tc.transactf("ok", "uid fetch 1:2 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
|
|
tc.transactf("ok", "uid fetch 1,2 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, bodystructure1}}, imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
|
|
tc.transactf("ok", "uid fetch 2:2 bodystructure")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, bodystructure2}})
|
|
|
|
// todo: read the bodies/headers of the parts, and of the nested message.
|
|
tc.transactf("ok", "fetch 2 body.peek[]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, 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", "fetch 2 body.peek[1]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[1]", Section: "1", Body: part1}}})
|
|
|
|
tc.transactf("no", "fetch 2 binary.peek[3]") // Only allowed on leaf parts, not multiparts.
|
|
tc.transactf("no", "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", "fetch 2 binary.size[3.1]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBinarySize{RespAttr: "BINARY.SIZE[3.1]", Parts: []uint32{3, 1}, Size: int64(len(part31dec))}}})
|
|
|
|
tc.transactf("ok", "fetch 2 body.peek[3.1]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[3.1]", Section: "3.1", Body: part31}}})
|
|
|
|
tc.transactf("ok", "fetch 2 binary.peek[3.1]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, 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", "fetch 2 body.peek[3]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[3]", Section: "3", Body: part3}}})
|
|
|
|
part2mime := tocrlf(`Content-type: text/plain; charset=US-ASCII
|
|
|
|
`)
|
|
tc.transactf("ok", "fetch 2 body.peek[2.mime]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, 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", "fetch 2 body.peek[5]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, 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", "fetch 2 body.peek[5.header]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.HEADER]", Section: "5.HEADER", Body: part5header}}})
|
|
|
|
part5mime := tocrlf(`Content-Type: Text/plain; charset=ISO-8859-1
|
|
Content-Transfer-Encoding: Quoted-printable
|
|
|
|
`)
|
|
tc.transactf("ok", "fetch 2 body.peek[5.mime]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, 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", "fetch 2 body.peek[5.text]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.TEXT]", Section: "5.TEXT", Body: part5text}}})
|
|
|
|
tc.transactf("ok", "fetch 2 body.peek[5.1]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, imapclient.FetchBody{RespAttr: "BODY[5.1]", Section: "5.1", Body: part5text}}})
|
|
|
|
// In case of EXAMINE instead of SELECT, we should not be seeing any changed \Seen flags for non-peek commands.
|
|
tc.client.StoreFlagsClear("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", "fetch 1 preview")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, imapclient.FetchPreview{Preview: &preview}}})
|
|
|
|
tc.transactf("ok", "fetch 1 preview (lazy)")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, 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", "fetch 1 preview")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, 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", "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)
|
|
defer tc2.closeNoWait()
|
|
tc2.client.Login("mjl@mox.example", password0)
|
|
tc2.client.Select("inbox")
|
|
tc2.client.StoreFlagsSet("1", true, `\Deleted`)
|
|
tc2.client.Expunge()
|
|
tc2.client.Logout()
|
|
|
|
tc.transactf("ok", "fetch 1 binary[]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, binary1}})
|
|
|
|
tc.transactf("ok", "fetch 1 body[]")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, body1}})
|
|
|
|
tc.transactf("ok", "fetch 1 rfc822.text")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfctext1}})
|
|
|
|
tc.transactf("ok", "fetch 1 rfc822")
|
|
tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, rfc1}})
|
|
|
|
tc.client.Logout()
|
|
}
|