1package imapserver
2
3// todo: if fetch fails part-way through the command, we wouldn't be storing the messages that were parsed. should we try harder to get parsed form of messages stored in db?
4
5import (
6 "bytes"
7 "errors"
8 "fmt"
9 "io"
10 "net/textproto"
11 "sort"
12 "strings"
13
14 "golang.org/x/exp/maps"
15
16 "github.com/mjl-/bstore"
17
18 "github.com/mjl-/mox/message"
19 "github.com/mjl-/mox/mlog"
20 "github.com/mjl-/mox/moxio"
21 "github.com/mjl-/mox/moxvar"
22 "github.com/mjl-/mox/store"
23)
24
25// functions to handle fetch attribute requests are defined on fetchCmd.
26type fetchCmd struct {
27 conn *conn
28 mailboxID int64
29 uid store.UID
30 tx *bstore.Tx // Writable tx, for storing message when first parsed as mime parts.
31 changes []store.Change // For updated Seen flag.
32 markSeen bool
33 needFlags bool
34 needModseq bool // Whether untagged responses needs modseq.
35 expungeIssued bool // Set if a message cannot be read. Can happen for expunged messages.
36 modseq store.ModSeq // Initialized on first change, for marking messages as seen.
37 isUID bool // If this is a UID FETCH command.
38 hasChangedSince bool // Whether CHANGEDSINCE was set. Enables MODSEQ in response.
39 deltaCounts store.MailboxCounts // By marking \Seen, the number of unread/unseen messages will go down. We update counts at the end.
40
41 // Loaded when first needed, closed when message was processed.
42 m *store.Message // Message currently being processed.
43 msgr *store.MsgReader
44 part *message.Part
45}
46
47// error when processing an attribute. we typically just don't respond with requested attributes that encounter a failure.
48type attrError struct{ err error }
49
50func (e attrError) Error() string {
51 return e.err.Error()
52}
53
54// raise error processing an attribute.
55func (cmd *fetchCmd) xerrorf(format string, args ...any) {
56 panic(attrError{fmt.Errorf(format, args...)})
57}
58
59func (cmd *fetchCmd) xcheckf(err error, format string, args ...any) {
60 if err != nil {
61 msg := fmt.Sprintf(format, args...)
62 cmd.xerrorf("%s: %w", msg, err)
63 }
64}
65
66// Fetch returns information about messages, be it email envelopes, headers,
67// bodies, full messages, flags.
68//
69// State: Selected
70func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
71 // Command: ../rfc/9051:4330 ../rfc/3501:2992 ../rfc/7162:864
72 // Examples: ../rfc/9051:4463 ../rfc/9051:4520 ../rfc/7162:880
73 // Response syntax: ../rfc/9051:6742 ../rfc/3501:4864 ../rfc/7162:2490
74
75 // Request syntax: ../rfc/9051:6553 ../rfc/3501:4748 ../rfc/4466:535 ../rfc/7162:2475
76 p.xspace()
77 nums := p.xnumSet()
78 p.xspace()
79 atts := p.xfetchAtts(isUID)
80 var changedSince int64
81 var haveChangedSince bool
82 var vanished bool
83 if p.space() {
84 // ../rfc/4466:542
85 // ../rfc/7162:2479
86 p.xtake("(")
87 seen := map[string]bool{}
88 for {
89 var w string
90 if isUID && p.conn.enabled[capQresync] {
91 // Vanished only valid for uid fetch, and only for qresync. ../rfc/7162:1693
92 w = p.xtakelist("CHANGEDSINCE", "VANISHED")
93 } else {
94 w = p.xtakelist("CHANGEDSINCE")
95 }
96 if seen[w] {
97 xsyntaxErrorf("duplicate fetch modifier %s", w)
98 }
99 seen[w] = true
100 switch w {
101 case "CHANGEDSINCE":
102 p.xspace()
103 changedSince = p.xnumber64()
104 // workaround: ios mail (16.5.1) was seen sending changedSince 0 on an existing account that got condstore enabled.
105 if changedSince == 0 && moxvar.Pedantic {
106 // ../rfc/7162:2551
107 xsyntaxErrorf("changedsince modseq must be > 0")
108 }
109 // CHANGEDSINCE is a CONDSTORE-enabling parameter. ../rfc/7162:380
110 p.conn.xensureCondstore(nil)
111 haveChangedSince = true
112 case "VANISHED":
113 vanished = true
114 }
115 if p.take(")") {
116 break
117 }
118 p.xspace()
119 }
120
121 // ../rfc/7162:1701
122 if vanished && !haveChangedSince {
123 xsyntaxErrorf("VANISHED can only be used with CHANGEDSINCE")
124 }
125 }
126 p.xempty()
127
128 // We don't use c.account.WithRLock because we write to the client while reading messages.
129 // We get the rlock, then we check the mailbox, release the lock and read the messages.
130 // The db transaction still locks out any changes to the database...
131 c.account.RLock()
132 runlock := c.account.RUnlock
133 // Note: we call runlock in a closure because we replace it below.
134 defer func() {
135 runlock()
136 }()
137
138 var vanishedUIDs []store.UID
139 cmd := &fetchCmd{conn: c, mailboxID: c.mailboxID, isUID: isUID, hasChangedSince: haveChangedSince}
140 c.xdbwrite(func(tx *bstore.Tx) {
141 cmd.tx = tx
142
143 // Ensure the mailbox still exists.
144 mb := c.xmailboxID(tx, c.mailboxID)
145
146 var uids []store.UID
147
148 // With changedSince, the client is likely asking for a small set of changes. Use a
149 // database query to trim down the uids we need to look at.
150 // ../rfc/7162:871
151 if changedSince > 0 {
152 q := bstore.QueryTx[store.Message](tx)
153 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
154 q.FilterGreater("ModSeq", store.ModSeqFromClient(changedSince))
155 if !vanished {
156 q.FilterEqual("Expunged", false)
157 }
158 err := q.ForEach(func(m store.Message) error {
159 if m.Expunged {
160 vanishedUIDs = append(vanishedUIDs, m.UID)
161 } else if isUID {
162 if nums.containsUID(m.UID, c.uids, c.searchResult) {
163 uids = append(uids, m.UID)
164 }
165 } else {
166 seq := c.sequence(m.UID)
167 if seq > 0 && nums.containsSeq(seq, c.uids, c.searchResult) {
168 uids = append(uids, m.UID)
169 }
170 }
171 return nil
172 })
173 xcheckf(err, "looking up messages with changedsince")
174 } else {
175 uids = c.xnumSetUIDs(isUID, nums)
176 }
177
178 // Send vanished for all missing requested UIDs. ../rfc/7162:1718
179 if vanished {
180 delModSeq, err := c.account.HighestDeletedModSeq(tx)
181 xcheckf(err, "looking up highest deleted modseq")
182 if changedSince < delModSeq.Client() {
183 // First sort the uids we already found, for fast lookup.
184 sort.Slice(vanishedUIDs, func(i, j int) bool {
185 return vanishedUIDs[i] < vanishedUIDs[j]
186 })
187
188 // We'll be gathering any more vanished uids in more.
189 more := map[store.UID]struct{}{}
190 checkVanished := func(uid store.UID) {
191 if uidSearch(c.uids, uid) <= 0 && uidSearch(vanishedUIDs, uid) <= 0 {
192 more[uid] = struct{}{}
193 }
194 }
195 // Now look through the requested uids. We may have a searchResult, handle it
196 // separately from a numset with potential stars, over which we can more easily
197 // iterate.
198 if nums.searchResult {
199 for _, uid := range c.searchResult {
200 checkVanished(uid)
201 }
202 } else {
203 iter := nums.interpretStar(c.uids).newIter()
204 for {
205 num, ok := iter.Next()
206 if !ok {
207 break
208 }
209 checkVanished(store.UID(num))
210 }
211 }
212 vanishedUIDs = append(vanishedUIDs, maps.Keys(more)...)
213 }
214 }
215
216 // Release the account lock.
217 runlock()
218 runlock = func() {} // Prevent defer from unlocking again.
219
220 // First report all vanished UIDs. ../rfc/7162:1714
221 if len(vanishedUIDs) > 0 {
222 // Mention all vanished UIDs in compact numset form.
223 // ../rfc/7162:1985
224 sort.Slice(vanishedUIDs, func(i, j int) bool {
225 return vanishedUIDs[i] < vanishedUIDs[j]
226 })
227 // No hard limit on response sizes, but clients are recommended to not send more
228 // than 8k. We send a more conservative max 4k.
229 for _, s := range compactUIDSet(vanishedUIDs).Strings(4*1024 - 32) {
230 c.bwritelinef("* VANISHED (EARLIER) %s", s)
231 }
232 }
233
234 for _, uid := range uids {
235 cmd.uid = uid
236 mlog.Field("processing uid", mlog.Field("uid", uid))
237 cmd.process(atts)
238 }
239
240 var zeromc store.MailboxCounts
241 if cmd.deltaCounts != zeromc {
242 mb.Add(cmd.deltaCounts) // Unseen/Unread will be <= 0.
243 err := tx.Update(&mb)
244 xcheckf(err, "updating mailbox counts")
245 cmd.changes = append(cmd.changes, mb.ChangeCounts())
246 }
247 })
248
249 if len(cmd.changes) > 0 {
250 // Broadcast seen updates to other connections.
251 c.broadcast(cmd.changes)
252 }
253
254 if cmd.expungeIssued {
255 // ../rfc/2180:343
256 c.writeresultf("%s NO [EXPUNGEISSUED] at least one message was expunged", tag)
257 } else {
258 c.ok(tag, cmdstr)
259 }
260}
261
262func (cmd *fetchCmd) xmodseq() store.ModSeq {
263 if cmd.modseq == 0 {
264 var err error
265 cmd.modseq, err = cmd.conn.account.NextModSeq(cmd.tx)
266 cmd.xcheckf(err, "assigning next modseq")
267 }
268 return cmd.modseq
269}
270
271func (cmd *fetchCmd) xensureMessage() *store.Message {
272 if cmd.m != nil {
273 return cmd.m
274 }
275
276 q := bstore.QueryTx[store.Message](cmd.tx)
277 q.FilterNonzero(store.Message{MailboxID: cmd.mailboxID, UID: cmd.uid})
278 q.FilterEqual("Expunged", false)
279 m, err := q.Get()
280 cmd.xcheckf(err, "get message for uid %d", cmd.uid)
281 cmd.m = &m
282 return cmd.m
283}
284
285func (cmd *fetchCmd) xensureParsed() (*store.MsgReader, *message.Part) {
286 if cmd.msgr != nil {
287 return cmd.msgr, cmd.part
288 }
289
290 m := cmd.xensureMessage()
291
292 cmd.msgr = cmd.conn.account.MessageReader(*m)
293 defer func() {
294 if cmd.part == nil {
295 err := cmd.msgr.Close()
296 cmd.conn.xsanity(err, "closing messagereader")
297 cmd.msgr = nil
298 }
299 }()
300
301 p, err := m.LoadPart(cmd.msgr)
302 xcheckf(err, "load parsed message")
303 cmd.part = &p
304 return cmd.msgr, cmd.part
305}
306
307func (cmd *fetchCmd) process(atts []fetchAtt) {
308 defer func() {
309 cmd.m = nil
310 cmd.part = nil
311 if cmd.msgr != nil {
312 err := cmd.msgr.Close()
313 cmd.conn.xsanity(err, "closing messagereader")
314 cmd.msgr = nil
315 }
316
317 x := recover()
318 if x == nil {
319 return
320 }
321 err, ok := x.(attrError)
322 if !ok {
323 panic(x)
324 }
325 if errors.Is(err, bstore.ErrAbsent) {
326 cmd.expungeIssued = true
327 return
328 }
329 cmd.conn.log.Infox("processing fetch attribute", err, mlog.Field("uid", cmd.uid))
330 xuserErrorf("processing fetch attribute: %v", err)
331 }()
332
333 data := listspace{bare("UID"), number(cmd.uid)}
334
335 cmd.markSeen = false
336 cmd.needFlags = false
337 cmd.needModseq = false
338
339 for _, a := range atts {
340 data = append(data, cmd.xprocessAtt(a)...)
341 }
342
343 if cmd.markSeen {
344 m := cmd.xensureMessage()
345 cmd.deltaCounts.Sub(m.MailboxCounts())
346 origFlags := m.Flags
347 m.Seen = true
348 cmd.deltaCounts.Add(m.MailboxCounts())
349 m.ModSeq = cmd.xmodseq()
350 err := cmd.tx.Update(m)
351 xcheckf(err, "marking message as seen")
352
353 cmd.changes = append(cmd.changes, m.ChangeFlags(origFlags))
354 }
355
356 if cmd.needFlags {
357 m := cmd.xensureMessage()
358 data = append(data, bare("FLAGS"), flaglist(m.Flags, m.Keywords))
359 }
360
361 // The wording around when to include the MODSEQ attribute is hard to follow and is
362 // specified and refined in several places.
363 //
364 // An additional rule applies to "QRESYNC servers" (we'll assume it only applies
365 // when QRESYNC is enabled on a connection): setting the \Seen flag also triggers
366 // sending MODSEQ, and so does a UID FETCH command. ../rfc/7162:1421
367 //
368 // For example, ../rfc/7162:389 says the server must include modseq in "all
369 // subsequent untagged fetch responses", then lists cases, but leaves out FETCH/UID
370 // FETCH. That appears intentional, it is not a list of examples, it is the full
371 // list, and the "all subsequent untagged fetch responses" doesn't mean "all", just
372 // those covering the listed cases. That makes sense, because otherwise all the
373 // other mentioning of cases elsewhere in the RFC would be too superfluous.
374 //
375 // ../rfc/7162:877 ../rfc/7162:388 ../rfc/7162:909 ../rfc/7162:1426
376 if cmd.needModseq || cmd.hasChangedSince || cmd.conn.enabled[capQresync] && (cmd.isUID || cmd.markSeen) {
377 m := cmd.xensureMessage()
378 data = append(data, bare("MODSEQ"), listspace{bare(fmt.Sprintf("%d", m.ModSeq.Client()))})
379 }
380
381 // Write errors are turned into panics because we write through c.
382 fmt.Fprintf(cmd.conn.bw, "* %d FETCH ", cmd.conn.xsequence(cmd.uid))
383 data.writeTo(cmd.conn, cmd.conn.bw)
384 cmd.conn.bw.Write([]byte("\r\n"))
385}
386
387// result for one attribute. if processing fails, e.g. because data was requested
388// that doesn't exist and cannot be represented in imap, the attribute is simply
389// not returned to the user. in this case, the returned value is a nil list.
390func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token {
391 switch a.field {
392 case "UID":
393 // Always present.
394 return nil
395 case "ENVELOPE":
396 _, part := cmd.xensureParsed()
397 envelope := xenvelope(part)
398 return []token{bare("ENVELOPE"), envelope}
399
400 case "INTERNALDATE":
401 // ../rfc/9051:6753 ../rfc/9051:6502
402 m := cmd.xensureMessage()
403 return []token{bare("INTERNALDATE"), dquote(m.Received.Format("_2-Jan-2006 15:04:05 -0700"))}
404
405 case "BODYSTRUCTURE":
406 _, part := cmd.xensureParsed()
407 bs := xbodystructure(part)
408 return []token{bare("BODYSTRUCTURE"), bs}
409
410 case "BODY":
411 respField, t := cmd.xbody(a)
412 if respField == "" {
413 return nil
414 }
415 return []token{bare(respField), t}
416
417 case "BINARY.SIZE":
418 _, p := cmd.xensureParsed()
419 if len(a.sectionBinary) == 0 {
420 // Must return the size of the entire message but with decoded body.
421 // todo: make this less expensive and/or cache the result?
422 n, err := io.Copy(io.Discard, cmd.xbinaryMessageReader(p))
423 cmd.xcheckf(err, "reading message as binary for its size")
424 return []token{bare(cmd.sectionRespField(a)), number(uint32(n))}
425 }
426 p = cmd.xpartnumsDeref(a.sectionBinary, p)
427 if len(p.Parts) > 0 || p.Message != nil {
428 // ../rfc/9051:4385
429 cmd.xerrorf("binary only allowed on leaf parts, not multipart/* or message/rfc822 or message/global")
430 }
431 return []token{bare(cmd.sectionRespField(a)), number(p.DecodedSize)}
432
433 case "BINARY":
434 respField, t := cmd.xbinary(a)
435 if respField == "" {
436 return nil
437 }
438 return []token{bare(respField), t}
439
440 case "RFC822.SIZE":
441 m := cmd.xensureMessage()
442 return []token{bare("RFC822.SIZE"), number(m.Size)}
443
444 case "RFC822.HEADER":
445 ba := fetchAtt{
446 field: "BODY",
447 peek: true,
448 section: &sectionSpec{
449 msgtext: &sectionMsgtext{s: "HEADER"},
450 },
451 }
452 respField, t := cmd.xbody(ba)
453 if respField == "" {
454 return nil
455 }
456 return []token{bare(a.field), t}
457
458 case "RFC822":
459 ba := fetchAtt{
460 field: "BODY",
461 section: &sectionSpec{},
462 }
463 respField, t := cmd.xbody(ba)
464 if respField == "" {
465 return nil
466 }
467 return []token{bare(a.field), t}
468
469 case "RFC822.TEXT":
470 ba := fetchAtt{
471 field: "BODY",
472 section: &sectionSpec{
473 msgtext: &sectionMsgtext{s: "TEXT"},
474 },
475 }
476 respField, t := cmd.xbody(ba)
477 if respField == "" {
478 return nil
479 }
480 return []token{bare(a.field), t}
481
482 case "FLAGS":
483 cmd.needFlags = true
484
485 case "MODSEQ":
486 cmd.needModseq = true
487
488 default:
489 xserverErrorf("field %q not yet implemented", a.field)
490 }
491 return nil
492}
493
494// ../rfc/9051:6522
495func xenvelope(p *message.Part) token {
496 var env message.Envelope
497 if p.Envelope != nil {
498 env = *p.Envelope
499 }
500 var date token = nilt
501 if !env.Date.IsZero() {
502 // ../rfc/5322:791
503 date = string0(env.Date.Format("Mon, 2 Jan 2006 15:04:05 -0700"))
504 }
505 var subject token = nilt
506 if env.Subject != "" {
507 subject = string0(env.Subject)
508 }
509 var inReplyTo token = nilt
510 if env.InReplyTo != "" {
511 inReplyTo = string0(env.InReplyTo)
512 }
513 var messageID token = nilt
514 if env.MessageID != "" {
515 messageID = string0(env.MessageID)
516 }
517
518 addresses := func(l []message.Address) token {
519 if len(l) == 0 {
520 return nilt
521 }
522 r := listspace{}
523 for _, a := range l {
524 var name token = nilt
525 if a.Name != "" {
526 name = string0(a.Name)
527 }
528 user := string0(a.User)
529 var host token = nilt
530 if a.Host != "" {
531 host = string0(a.Host)
532 }
533 r = append(r, listspace{name, nilt, user, host})
534 }
535 return r
536 }
537
538 // Empty sender or reply-to result in fall-back to from. ../rfc/9051:6140
539 sender := env.Sender
540 if len(sender) == 0 {
541 sender = env.From
542 }
543 replyTo := env.ReplyTo
544 if len(replyTo) == 0 {
545 replyTo = env.From
546 }
547
548 return listspace{
549 date,
550 subject,
551 addresses(env.From),
552 addresses(sender),
553 addresses(replyTo),
554 addresses(env.To),
555 addresses(env.CC),
556 addresses(env.BCC),
557 inReplyTo,
558 messageID,
559 }
560}
561
562func (cmd *fetchCmd) peekOrSeen(peek bool) {
563 if cmd.conn.readonly || peek {
564 return
565 }
566 m := cmd.xensureMessage()
567 if !m.Seen {
568 cmd.markSeen = true
569 cmd.needFlags = true
570 }
571}
572
573// reader that returns the message, but with header Content-Transfer-Encoding left out.
574func (cmd *fetchCmd) xbinaryMessageReader(p *message.Part) io.Reader {
575 hr := cmd.xmodifiedHeader(p, []string{"Content-Transfer-Encoding"}, true)
576 return io.MultiReader(hr, p.Reader())
577}
578
579// return header with only fields, or with everything except fields if "not" is set.
580func (cmd *fetchCmd) xmodifiedHeader(p *message.Part, fields []string, not bool) io.Reader {
581 h, err := io.ReadAll(p.HeaderReader())
582 cmd.xcheckf(err, "reading header")
583
584 matchesFields := func(line []byte) bool {
585 k := bytes.TrimRight(bytes.SplitN(line, []byte(":"), 2)[0], " \t")
586 for _, f := range fields {
587 if bytes.EqualFold(k, []byte(f)) {
588 return true
589 }
590 }
591 return false
592 }
593
594 var match bool
595 hb := &bytes.Buffer{}
596 for len(h) > 0 {
597 line := h
598 i := bytes.Index(line, []byte("\r\n"))
599 if i >= 0 {
600 line = line[:i+2]
601 }
602 h = h[len(line):]
603
604 match = matchesFields(line) || match && (bytes.HasPrefix(line, []byte(" ")) || bytes.HasPrefix(line, []byte("\t")))
605 if match != not || len(line) == 2 {
606 hb.Write(line)
607 }
608 }
609 return hb
610}
611
612func (cmd *fetchCmd) xbinary(a fetchAtt) (string, token) {
613 _, part := cmd.xensureParsed()
614
615 cmd.peekOrSeen(a.peek)
616 if len(a.sectionBinary) == 0 {
617 r := cmd.xbinaryMessageReader(part)
618 if a.partial != nil {
619 r = cmd.xpartialReader(a.partial, r)
620 }
621 return cmd.sectionRespField(a), readerSyncliteral{r}
622 }
623
624 p := part
625 if len(a.sectionBinary) > 0 {
626 p = cmd.xpartnumsDeref(a.sectionBinary, p)
627 }
628 if len(p.Parts) != 0 || p.Message != nil {
629 // ../rfc/9051:4385
630 cmd.xerrorf("binary only allowed on leaf parts, not multipart/* or message/rfc822 or message/global")
631 }
632
633 switch p.ContentTransferEncoding {
634 case "", "7BIT", "8BIT", "BINARY", "BASE64", "QUOTED-PRINTABLE":
635 default:
636 // ../rfc/9051:5913
637 xusercodeErrorf("UNKNOWN-CTE", "unknown Content-Transfer-Encoding %q", p.ContentTransferEncoding)
638 }
639
640 r := p.Reader()
641 if a.partial != nil {
642 r = cmd.xpartialReader(a.partial, r)
643 }
644 return cmd.sectionRespField(a), readerSyncliteral{r}
645}
646
647func (cmd *fetchCmd) xpartialReader(partial *partial, r io.Reader) io.Reader {
648 n, err := io.Copy(io.Discard, io.LimitReader(r, int64(partial.offset)))
649 cmd.xcheckf(err, "skipping to offset for partial")
650 if n != int64(partial.offset) {
651 return strings.NewReader("") // ../rfc/3501:3143 ../rfc/9051:4418
652 }
653 return io.LimitReader(r, int64(partial.count))
654}
655
656func (cmd *fetchCmd) xbody(a fetchAtt) (string, token) {
657 msgr, part := cmd.xensureParsed()
658
659 if a.section == nil {
660 // Non-extensible form of BODYSTRUCTURE.
661 return a.field, xbodystructure(part)
662 }
663
664 cmd.peekOrSeen(a.peek)
665
666 respField := cmd.sectionRespField(a)
667
668 if a.section.msgtext == nil && a.section.part == nil {
669 m := cmd.xensureMessage()
670 var offset int64
671 count := m.Size
672 if a.partial != nil {
673 offset = int64(a.partial.offset)
674 if offset > m.Size {
675 offset = m.Size
676 }
677 count = int64(a.partial.count)
678 if offset+count > m.Size {
679 count = m.Size - offset
680 }
681 }
682 return respField, readerSizeSyncliteral{&moxio.AtReader{R: msgr, Offset: offset}, count}
683 }
684
685 sr := cmd.xsection(a.section, part)
686
687 if a.partial != nil {
688 n, err := io.Copy(io.Discard, io.LimitReader(sr, int64(a.partial.offset)))
689 cmd.xcheckf(err, "skipping to offset for partial")
690 if n != int64(a.partial.offset) {
691 return respField, syncliteral("") // ../rfc/3501:3143 ../rfc/9051:4418
692 }
693 return respField, readerSyncliteral{io.LimitReader(sr, int64(a.partial.count))}
694 }
695 return respField, readerSyncliteral{sr}
696}
697
698func (cmd *fetchCmd) xpartnumsDeref(nums []uint32, p *message.Part) *message.Part {
699 // ../rfc/9051:4481
700 if (len(p.Parts) == 0 && p.Message == nil) && len(nums) == 1 && nums[0] == 1 {
701 return p
702 }
703
704 // ../rfc/9051:4485
705 for i, num := range nums {
706 index := int(num - 1)
707 if p.Message != nil {
708 err := p.SetMessageReaderAt()
709 cmd.xcheckf(err, "preparing submessage")
710 return cmd.xpartnumsDeref(nums[i:], p.Message)
711 }
712 if index < 0 || index >= len(p.Parts) {
713 cmd.xerrorf("requested part does not exist")
714 }
715 p = &p.Parts[index]
716 }
717 return p
718}
719
720func (cmd *fetchCmd) xsection(section *sectionSpec, p *message.Part) io.Reader {
721 if section.part == nil {
722 return cmd.xsectionMsgtext(section.msgtext, p)
723 }
724
725 p = cmd.xpartnumsDeref(section.part.part, p)
726
727 if section.part.text == nil {
728 return p.RawReader()
729 }
730
731 // ../rfc/9051:4535
732 if p.Message != nil {
733 err := p.SetMessageReaderAt()
734 cmd.xcheckf(err, "preparing submessage")
735 p = p.Message
736 }
737
738 if !section.part.text.mime {
739 return cmd.xsectionMsgtext(section.part.text.msgtext, p)
740 }
741
742 // MIME header, see ../rfc/9051:4534 ../rfc/2045:1645
743 h, err := io.ReadAll(p.HeaderReader())
744 cmd.xcheckf(err, "reading header")
745
746 matchesFields := func(line []byte) bool {
747 k := textproto.CanonicalMIMEHeaderKey(string(bytes.TrimRight(bytes.SplitN(line, []byte(":"), 2)[0], " \t")))
748 // Only add MIME-Version and additional CRLF for messages, not other parts. ../rfc/2045:1645 ../rfc/2045:1652
749 return (p.Envelope != nil && k == "Mime-Version") || strings.HasPrefix(k, "Content-")
750 }
751
752 var match bool
753 hb := &bytes.Buffer{}
754 for len(h) > 0 {
755 line := h
756 i := bytes.Index(line, []byte("\r\n"))
757 if i >= 0 {
758 line = line[:i+2]
759 }
760 h = h[len(line):]
761
762 match = matchesFields(line) || match && (bytes.HasPrefix(line, []byte(" ")) || bytes.HasPrefix(line, []byte("\t")))
763 if match || len(line) == 2 {
764 hb.Write(line)
765 }
766 }
767 return hb
768}
769
770func (cmd *fetchCmd) xsectionMsgtext(smt *sectionMsgtext, p *message.Part) io.Reader {
771 if smt.s == "HEADER" {
772 return p.HeaderReader()
773 }
774
775 switch smt.s {
776 case "HEADER.FIELDS":
777 return cmd.xmodifiedHeader(p, smt.headers, false)
778
779 case "HEADER.FIELDS.NOT":
780 return cmd.xmodifiedHeader(p, smt.headers, true)
781
782 case "TEXT":
783 // It appears imap clients expect to get the body of the message, not a "text body"
784 // which sounds like it means a text/* part of a message. ../rfc/9051:4517
785 return p.RawReader()
786 }
787 panic(serverError{fmt.Errorf("missing case")})
788}
789
790func (cmd *fetchCmd) sectionRespField(a fetchAtt) string {
791 s := a.field + "["
792 if len(a.sectionBinary) > 0 {
793 s += fmt.Sprintf("%d", a.sectionBinary[0])
794 for _, v := range a.sectionBinary[1:] {
795 s += "." + fmt.Sprintf("%d", v)
796 }
797 } else if a.section != nil {
798 if a.section.part != nil {
799 p := a.section.part
800 s += fmt.Sprintf("%d", p.part[0])
801 for _, v := range p.part[1:] {
802 s += "." + fmt.Sprintf("%d", v)
803 }
804 if p.text != nil {
805 if p.text.mime {
806 s += ".MIME"
807 } else {
808 s += "." + cmd.sectionMsgtextName(p.text.msgtext)
809 }
810 }
811 } else if a.section.msgtext != nil {
812 s += cmd.sectionMsgtextName(a.section.msgtext)
813 }
814 }
815 s += "]"
816 // binary does not have partial in field, unlike BODY ../rfc/9051:6757
817 if a.field != "BINARY" && a.partial != nil {
818 s += fmt.Sprintf("<%d>", a.partial.offset)
819 }
820 return s
821}
822
823func (cmd *fetchCmd) sectionMsgtextName(smt *sectionMsgtext) string {
824 s := smt.s
825 if strings.HasPrefix(smt.s, "HEADER.FIELDS") {
826 l := listspace{}
827 for _, h := range smt.headers {
828 l = append(l, astring(h))
829 }
830 s += " " + l.pack(cmd.conn)
831 }
832 return s
833}
834
835func bodyFldParams(params map[string]string) token {
836 if len(params) == 0 {
837 return nilt
838 }
839 // Ensure same ordering, easier for testing.
840 var keys []string
841 for k := range params {
842 keys = append(keys, k)
843 }
844 sort.Strings(keys)
845 l := make(listspace, 2*len(keys))
846 i := 0
847 for _, k := range keys {
848 l[i] = string0(strings.ToUpper(k))
849 l[i+1] = string0(params[k])
850 i += 2
851 }
852 return l
853}
854
855func bodyFldEnc(s string) token {
856 up := strings.ToUpper(s)
857 switch up {
858 case "7BIT", "8BIT", "BINARY", "BASE64", "QUOTED-PRINTABLE":
859 return dquote(up)
860 }
861 return string0(s)
862}
863
864// xbodystructure returns a "body".
865// calls itself for multipart messages and message/{rfc822,global}.
866func xbodystructure(p *message.Part) token {
867 if p.MediaType == "MULTIPART" {
868 // Multipart, ../rfc/9051:6355 ../rfc/9051:6411
869 var bodies concat
870 for i := range p.Parts {
871 bodies = append(bodies, xbodystructure(&p.Parts[i]))
872 }
873 return listspace{bodies, string0(p.MediaSubType)}
874 }
875
876 // ../rfc/9051:6355
877 if p.MediaType == "TEXT" {
878 // ../rfc/9051:6404 ../rfc/9051:6418
879 return listspace{
880 dquote("TEXT"), string0(p.MediaSubType), // ../rfc/9051:6739
881 // ../rfc/9051:6376
882 bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401
883 nilOrString(p.ContentID),
884 nilOrString(p.ContentDescription),
885 bodyFldEnc(p.ContentTransferEncoding),
886 number(p.EndOffset - p.BodyOffset),
887 number(p.RawLineCount),
888 }
889 } else if p.MediaType == "MESSAGE" && (p.MediaSubType == "RFC822" || p.MediaSubType == "GLOBAL") {
890 // ../rfc/9051:6415
891 // note: we don't have to prepare p.Message for reading, because we aren't going to read from it.
892 return listspace{
893 dquote("MESSAGE"), dquote(p.MediaSubType), // ../rfc/9051:6732
894 // ../rfc/9051:6376
895 bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401
896 nilOrString(p.ContentID),
897 nilOrString(p.ContentDescription),
898 bodyFldEnc(p.ContentTransferEncoding),
899 number(p.EndOffset - p.BodyOffset),
900 xenvelope(p.Message),
901 xbodystructure(p.Message),
902 number(p.RawLineCount), // todo: or mp.RawLineCount?
903 }
904 }
905 var media token
906 switch p.MediaType {
907 case "APPLICATION", "AUDIO", "IMAGE", "FONT", "MESSAGE", "MODEL", "VIDEO":
908 media = dquote(p.MediaType)
909 default:
910 media = string0(p.MediaType)
911 }
912 // ../rfc/9051:6404 ../rfc/9051:6407
913 return listspace{
914 media, string0(p.MediaSubType), // ../rfc/9051:6723
915 // ../rfc/9051:6376
916 bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401
917 nilOrString(p.ContentID),
918 nilOrString(p.ContentDescription),
919 bodyFldEnc(p.ContentTransferEncoding),
920 number(p.EndOffset - p.BodyOffset),
921 }
922}
923