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?
17 "github.com/mjl-/bstore"
19 "github.com/mjl-/mox/message"
20 "github.com/mjl-/mox/mlog"
21 "github.com/mjl-/mox/mox-"
22 "github.com/mjl-/mox/moxio"
23 "github.com/mjl-/mox/store"
26// functions to handle fetch attribute requests are defined on fetchCmd.
29 isUID bool // If this is a UID FETCH command.
30 rtx *bstore.Tx // Read-only transaction, kept open while processing all messages.
31 updateSeen []store.UID // To mark as seen after processing all messages. UID instead of message ID since moved messages keep their ID and insert a new ID in the original mailbox.
32 hasChangedSince bool // Whether CHANGEDSINCE was set. Enables MODSEQ in response.
33 expungeIssued bool // Set if any message has been expunged. Can happen for expunged messages.
35 // For message currently processing.
41 needModseq bool // Whether untagged responses needs modseq.
42 newPreviews map[store.UID]string // Save with messages when done.
44 // Loaded when first needed, closed when message was processed.
45 m *store.Message // Message currently being processed.
50// error when processing an attribute. we typically just don't respond with requested attributes that encounter a failure.
51type attrError struct{ err error }
53func (e attrError) Error() string {
57// raise error processing an attribute.
58func (cmd *fetchCmd) xerrorf(format string, args ...any) {
59 panic(attrError{fmt.Errorf(format, args...)})
62func (cmd *fetchCmd) xcheckf(err error, format string, args ...any) {
64 msg := fmt.Sprintf(format, args...)
65 cmd.xerrorf("%s: %w", msg, err)
69// Fetch returns information about messages, be it email envelopes, headers,
70// bodies, full messages, flags.
73func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
82 atts := p.xfetchAtts()
83 var changedSince int64
84 var haveChangedSince bool
90 seen := map[string]bool{}
93 if isUID && p.conn.enabled[capQresync] {
95 w = p.xtakelist("CHANGEDSINCE", "VANISHED")
97 w = p.xtakelist("CHANGEDSINCE")
100 xsyntaxErrorf("duplicate fetch modifier %s", w)
106 changedSince = p.xnumber64()
107 // workaround: ios mail (16.5.1) was seen sending changedSince 0 on an existing account that got condstore enabled.
108 if changedSince == 0 && mox.Pedantic {
110 xsyntaxErrorf("changedsince modseq must be > 0")
113 p.conn.xensureCondstore(nil)
114 haveChangedSince = true
125 if vanished && !haveChangedSince {
126 xsyntaxErrorf("VANISHED can only be used with CHANGEDSINCE")
131 // We only keep a wlock, only for initial checks and listing the uids. Then we
132 // unlock and work without a lock. So changes to the store can happen, and we need
133 // to deal with that. If we need to mark messages as seen, we do so after
134 // processing the fetch for all messages, in a single write transaction. We don't
135 // send untagged changes for those \seen flag changes before finishing this
136 // command, because we have to sequence all changes properly, and since we don't
137 // (want to) hold a wlock while processing messages (can be many!), other changes
138 // may have happened to the store. So instead, we'll silently mark messages as seen
139 // (the client should know this is happening anyway!), then broadcast the changes
140 // to everyone, including ourselves. A noop/idle command that may come next will
141 // return the \seen flag changes, in the correct order, with the correct modseq. We
142 // also cannot just apply pending changes while processing. It is not allowed at
143 // all for non-uid-fetch. It would also make life more complicated, e.g. we would
144 // perhaps have to check if newly added messages also match uid fetch set that was
148 var vanishedUIDs []store.UID
150 cmd := &fetchCmd{conn: c, isUID: isUID, hasChangedSince: haveChangedSince, mailboxID: c.mailboxID, newPreviews: map[store.UID]string{}}
156 err := cmd.rtx.Rollback()
157 c.log.Check(err, "rollback rtx")
161 c.account.WithRLock(func() {
163 cmd.rtx, err = c.account.DB.Begin(context.TODO(), false)
164 cmd.xcheckf(err, "begin transaction")
166 // Ensure the mailbox still exists.
167 c.xmailboxID(cmd.rtx, c.mailboxID)
169 // With changedSince, the client is likely asking for a small set of changes. Use a
170 // database query to trim down the uids we need to look at. We need to go through
171 // the database for "VANISHED (EARLIER)" anyway, to see UIDs that aren't in the
173 if changedSince > 0 {
174 q := bstore.QueryTx[store.Message](cmd.rtx)
175 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
176 q.FilterGreater("ModSeq", store.ModSeqFromClient(changedSince))
178 q.FilterEqual("Expunged", false)
180 err := q.ForEach(func(m store.Message) error {
181 if m.UID >= c.uidnext {
185 if nums.xcontainsKnownUID(m.UID, c.searchResult, func() store.UID { return c.uidnext - 1 }) {
187 vanishedUIDs = append(vanishedUIDs, m.UID)
189 uids = append(uids, m.UID)
193 seq := c.sequence(m.UID)
194 if seq > 0 && nums.containsSeq(seq, c.uids, c.searchResult) {
195 uids = append(uids, m.UID)
200 xcheckf(err, "looking up messages with changedsince")
202 // In case of vanished where we don't have the full history, we must send VANISHED
204 delModSeq, err := c.account.HighestDeletedModSeq(cmd.rtx)
205 xcheckf(err, "looking up highest deleted modseq")
206 if !vanished || changedSince >= delModSeq.Client() {
210 // We'll iterate through all UIDs in the numset, and add anything that isn't
211 // already in uids and vanishedUIDs. First sort the uids we already found, for fast
212 // lookup. We'll gather new UIDs in more, so we don't break the binary search.
213 slices.Sort(vanishedUIDs)
216 more := map[store.UID]struct{}{} // We'll add them at the end.
217 checkVanished := func(uid store.UID) {
218 if uid < c.uidnext && uidSearch(uids, uid) <= 0 && uidSearch(vanishedUIDs, uid) <= 0 {
219 more[uid] = struct{}{}
223 // Now look through the requested uids. We may have a searchResult, handle it
224 // separately from a numset with potential stars, over which we can more easily
226 if nums.searchResult {
227 for _, uid := range c.searchResult {
231 xlastUID := c.newCachedLastUID(cmd.rtx, c.mailboxID, func(xerr error) { xuserErrorf("%s", xerr) })
232 iter := nums.xinterpretStar(xlastUID).newIter()
234 num, ok := iter.Next()
238 checkVanished(store.UID(num))
241 vanishedUIDs = slices.AppendSeq(vanishedUIDs, maps.Keys(more))
242 slices.Sort(vanishedUIDs)
244 uids = c.xnumSetEval(cmd.rtx, isUID, nums)
248 // We are continuing without a lock, working off our snapshot of uids to process.
251 if len(vanishedUIDs) > 0 {
252 // Mention all vanished UIDs in compact numset form.
254 // No hard limit on response sizes, but clients are recommended to not send more
255 // than 8k. We send a more conservative max 4k.
256 for _, s := range compactUIDSet(vanishedUIDs).Strings(4*1024 - 32) {
257 c.xbwritelinef("* VANISHED (EARLIER) %s", s)
261 defer cmd.msgclose() // In case of panic.
263 for _, cmd.uid = range uids {
264 cmd.conn.log.Debug("processing uid", slog.Any("uid", cmd.uid))
265 data, err := cmd.process(atts)
267 cmd.conn.log.Infox("processing fetch attribute", err, slog.Any("uid", cmd.uid))
268 xuserErrorf("processing fetch attribute: %v", err)
273 fmt.Fprintf(cmd.conn.xbw, "* %d UIDFETCH ", cmd.uid)
275 fmt.Fprintf(cmd.conn.xbw, "* %d FETCH ", cmd.conn.xsequence(cmd.uid))
277 data.xwriteTo(cmd.conn, cmd.conn.xbw)
278 cmd.conn.xbw.Write([]byte("\r\n"))
283 // We've returned all data. Now we mark messages as seen in one go, in a new write
284 // transaction. We don't send untagged messages for the changes, since there may be
285 // unprocessed pending changes. Instead, we broadcast them to ourselve too, so a
286 // next noop/idle will return the flags to the client.
288 err := cmd.rtx.Rollback()
289 c.log.Check(err, "fetch read tx rollback")
293 // command, in a single transaction.
294 if len(cmd.updateSeen) > 0 || len(cmd.newPreviews) > 0 {
295 c.account.WithWLock(func() {
296 changes := make([]store.Change, 0, len(cmd.updateSeen)+1)
298 c.xdbwrite(func(wtx *bstore.Tx) {
299 mb, err := store.MailboxID(wtx, c.mailboxID)
300 if err == store.ErrMailboxExpunged {
301 xusercodeErrorf("NONEXISTENT", "mailbox has been expunged")
303 xcheckf(err, "get mailbox for updating counts after marking as seen")
305 var modseq store.ModSeq
307 for _, uid := range cmd.updateSeen {
308 m, err := bstore.QueryTx[store.Message](wtx).FilterNonzero(store.Message{MailboxID: c.mailboxID, UID: uid}).Get()
309 xcheckf(err, "get message")
311 // Message has been deleted in the mean time.
312 cmd.expungeIssued = true
316 // Message already marked as seen by another process.
321 modseq, err = c.account.NextModSeq(wtx)
322 xcheckf(err, "get next mod seq")
326 mb.Sub(m.MailboxCounts())
328 mb.Add(m.MailboxCounts())
329 changes = append(changes, m.ChangeFlags(oldFlags, mb))
333 xcheckf(err, "mark message as seen")
336 changes = append(changes, mb.ChangeCounts())
338 for uid, s := range cmd.newPreviews {
339 m, err := bstore.QueryTx[store.Message](wtx).FilterNonzero(store.Message{MailboxID: c.mailboxID, UID: uid}).Get()
340 xcheckf(err, "get message")
342 // Message has been deleted in the mean time.
343 cmd.expungeIssued = true
347 // note: we are not updating modseq.
351 xcheckf(err, "saving preview with message")
356 err = wtx.Update(&mb)
357 xcheckf(err, "update mailbox with counts and modseq")
361 // Broadcast these changes also to ourselves, so we'll send the updated flags, but
362 // in the correct order, after other changes.
363 store.BroadcastChanges(c.account, changes)
367 if cmd.expungeIssued {
370 c.xwriteresultf("%s OK [EXPUNGEISSUED] at least one message was expunged", tag)
376func (cmd *fetchCmd) xensureMessage() *store.Message {
381 // We do not filter by Expunged, the message may have been deleted in other
382 // sessions, but not in ours.
383 q := bstore.QueryTx[store.Message](cmd.rtx)
384 q.FilterNonzero(store.Message{MailboxID: cmd.mailboxID, UID: cmd.uid})
386 cmd.xcheckf(err, "get message for uid %d", cmd.uid)
389 cmd.expungeIssued = true
394func (cmd *fetchCmd) xensureParsed() (*store.MsgReader, *message.Part) {
396 return cmd.msgr, cmd.part
399 m := cmd.xensureMessage()
401 cmd.msgr = cmd.conn.account.MessageReader(*m)
404 err := cmd.msgr.Close()
405 cmd.conn.xsanity(err, "closing messagereader")
410 p, err := m.LoadPart(cmd.msgr)
411 xcheckf(err, "load parsed message")
413 return cmd.msgr, cmd.part
416// msgclose must be called after processing a message (after having written/used
417// its data), even in the case of a panic.
418func (cmd *fetchCmd) msgclose() {
422 err := cmd.msgr.Close()
423 cmd.conn.xsanity(err, "closing messagereader")
428func (cmd *fetchCmd) process(atts []fetchAtt) (rdata listspace, rerr error) {
434 err, ok := x.(attrError)
437 } else if rerr == nil {
443 if !cmd.conn.uidonly {
444 data = append(data, bare("UID"), number(cmd.uid))
448 cmd.needFlags = false
449 cmd.needModseq = false
451 for _, a := range atts {
452 data = append(data, cmd.xprocessAtt(a)...)
456 cmd.updateSeen = append(cmd.updateSeen, cmd.uid)
460 m := cmd.xensureMessage()
461 data = append(data, bare("FLAGS"), flaglist(m.Flags, m.Keywords))
464 // The wording around when to include the MODSEQ attribute is hard to follow and is
465 // specified and refined in several places.
467 // An additional rule applies to "QRESYNC servers" (we'll assume it only applies
468 // when QRESYNC is enabled on a connection): setting the \Seen flag also triggers
472 // subsequent untagged fetch responses", then lists cases, but leaves out FETCH/UID
473 // FETCH. That appears intentional, it is not a list of examples, it is the full
474 // list, and the "all subsequent untagged fetch responses" doesn't mean "all", just
475 // those covering the listed cases. That makes sense, because otherwise all the
476 // other mentioning of cases elsewhere in the RFC would be too superfluous.
479 if cmd.needModseq || cmd.hasChangedSince || cmd.conn.enabled[capQresync] && cmd.isUID {
480 m := cmd.xensureMessage()
481 data = append(data, bare("MODSEQ"), listspace{bare(fmt.Sprintf("%d", m.ModSeq.Client()))})
487// result for one attribute. if processing fails, e.g. because data was requested
488// that doesn't exist and cannot be represented in imap, the attribute is simply
489// not returned to the user. in this case, the returned value is a nil list.
490func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token {
493 // Present by default without uidonly. For uidonly, we only add it when explicitly
495 if cmd.conn.uidonly {
496 return []token{bare("UID"), number(cmd.uid)}
500 _, part := cmd.xensureParsed()
501 envelope := xenvelope(part)
502 return []token{bare("ENVELOPE"), envelope}
506 m := cmd.xensureMessage()
507 return []token{bare("INTERNALDATE"), dquote(m.Received.Format("_2-Jan-2006 15:04:05 -0700"))}
510 m := cmd.xensureMessage()
511 // For messages in storage from before we implemented this extension, we don't have
512 // a savedate, and we return nil. This is normally meant to be per mailbox, but
514 var savedate token = nilt
515 if m.SaveDate != nil {
516 savedate = dquote(m.SaveDate.Format("_2-Jan-2006 15:04:05 -0700"))
518 return []token{bare("SAVEDATE"), savedate}
520 case "BODYSTRUCTURE":
521 _, part := cmd.xensureParsed()
522 bs := xbodystructure(cmd.conn.log, part, true)
523 return []token{bare("BODYSTRUCTURE"), bs}
526 respField, t := cmd.xbody(a)
530 return []token{bare(respField), t}
533 _, p := cmd.xensureParsed()
534 if len(a.sectionBinary) == 0 {
535 // Must return the size of the entire message but with decoded body.
536 // todo: make this less expensive and/or cache the result?
537 n, err := io.Copy(io.Discard, cmd.xbinaryMessageReader(p))
538 cmd.xcheckf(err, "reading message as binary for its size")
539 return []token{bare(cmd.sectionRespField(a)), number(uint32(n))}
541 p = cmd.xpartnumsDeref(a.sectionBinary, p)
542 if len(p.Parts) > 0 || p.Message != nil {
544 cmd.xerrorf("binary only allowed on leaf parts, not multipart/* or message/rfc822 or message/global")
546 return []token{bare(cmd.sectionRespField(a)), number(p.DecodedSize)}
549 respField, t := cmd.xbinary(a)
553 return []token{bare(respField), t}
556 m := cmd.xensureMessage()
557 return []token{bare("RFC822.SIZE"), number(m.Size)}
559 case "RFC822.HEADER":
563 section: §ionSpec{
564 msgtext: §ionMsgtext{s: "HEADER"},
567 respField, t := cmd.xbody(ba)
571 return []token{bare(a.field), t}
576 section: §ionSpec{},
578 respField, t := cmd.xbody(ba)
582 return []token{bare(a.field), t}
587 section: §ionSpec{
588 msgtext: §ionMsgtext{s: "TEXT"},
591 respField, t := cmd.xbody(ba)
595 return []token{bare(a.field), t}
601 cmd.needModseq = true
604 m := cmd.xensureMessage()
606 // We ignore "lazy", generating the preview is fast enough.
608 // Get the preview. We'll save all generated previews in a single transaction at
610 _, p := cmd.xensureParsed()
611 s, err := p.Preview(cmd.conn.log)
612 cmd.xcheckf(err, "generating preview")
614 cmd.newPreviews[m.UID] = s
629 s = strings.TrimSpace(s)
632 return []token{bare(a.field), t}
635 xserverErrorf("field %q not yet implemented", a.field)
641func xenvelope(p *message.Part) token {
642 var env message.Envelope
643 if p.Envelope != nil {
646 var date token = nilt
647 if !env.Date.IsZero() {
649 date = string0(env.Date.Format("Mon, 2 Jan 2006 15:04:05 -0700"))
651 var subject token = nilt
652 if env.Subject != "" {
653 subject = string0(env.Subject)
655 var inReplyTo token = nilt
656 if env.InReplyTo != "" {
657 inReplyTo = string0(env.InReplyTo)
659 var messageID token = nilt
660 if env.MessageID != "" {
661 messageID = string0(env.MessageID)
664 addresses := func(l []message.Address) token {
669 for _, a := range l {
670 var name token = nilt
672 name = string0(a.Name)
674 user := string0(a.User)
675 var host token = nilt
677 host = string0(a.Host)
679 r = append(r, listspace{name, nilt, user, host})
686 if len(sender) == 0 {
689 replyTo := env.ReplyTo
690 if len(replyTo) == 0 {
708func (cmd *fetchCmd) peekOrSeen(peek bool) {
709 if cmd.conn.readonly || peek {
712 m := cmd.xensureMessage()
719// reader that returns the message, but with header Content-Transfer-Encoding left out.
720func (cmd *fetchCmd) xbinaryMessageReader(p *message.Part) io.Reader {
721 hr := cmd.xmodifiedHeader(p, []string{"Content-Transfer-Encoding"}, true)
722 return io.MultiReader(hr, p.Reader())
725// return header with only fields, or with everything except fields if "not" is set.
726func (cmd *fetchCmd) xmodifiedHeader(p *message.Part, fields []string, not bool) io.Reader {
727 h, err := io.ReadAll(p.HeaderReader())
728 cmd.xcheckf(err, "reading header")
730 matchesFields := func(line []byte) bool {
731 k := bytes.TrimRight(bytes.SplitN(line, []byte(":"), 2)[0], " \t")
732 for _, f := range fields {
733 if bytes.EqualFold(k, []byte(f)) {
741 hb := &bytes.Buffer{}
744 i := bytes.Index(line, []byte("\r\n"))
750 match = matchesFields(line) || match && (bytes.HasPrefix(line, []byte(" ")) || bytes.HasPrefix(line, []byte("\t")))
751 if match != not || len(line) == 2 {
758func (cmd *fetchCmd) xbinary(a fetchAtt) (string, token) {
759 _, part := cmd.xensureParsed()
761 cmd.peekOrSeen(a.peek)
762 if len(a.sectionBinary) == 0 {
763 r := cmd.xbinaryMessageReader(part)
764 if a.partial != nil {
765 r = cmd.xpartialReader(a.partial, r)
767 return cmd.sectionRespField(a), readerSyncliteral{r}
771 if len(a.sectionBinary) > 0 {
772 p = cmd.xpartnumsDeref(a.sectionBinary, p)
774 if len(p.Parts) != 0 || p.Message != nil {
776 cmd.xerrorf("binary only allowed on leaf parts, not multipart/* or message/rfc822 or message/global")
780 if p.ContentTransferEncoding != nil {
781 cte = *p.ContentTransferEncoding
784 case "", "7BIT", "8BIT", "BINARY", "BASE64", "QUOTED-PRINTABLE":
787 xusercodeErrorf("UNKNOWN-CTE", "unknown Content-Transfer-Encoding %q", cte)
791 if a.partial != nil {
792 r = cmd.xpartialReader(a.partial, r)
794 return cmd.sectionRespField(a), readerSyncliteral{r}
797func (cmd *fetchCmd) xpartialReader(partial *partial, r io.Reader) io.Reader {
798 n, err := io.Copy(io.Discard, io.LimitReader(r, int64(partial.offset)))
799 cmd.xcheckf(err, "skipping to offset for partial")
800 if n != int64(partial.offset) {
803 return io.LimitReader(r, int64(partial.count))
806func (cmd *fetchCmd) xbody(a fetchAtt) (string, token) {
807 msgr, part := cmd.xensureParsed()
809 if a.section == nil {
810 // Non-extensible form of BODYSTRUCTURE.
811 return a.field, xbodystructure(cmd.conn.log, part, false)
814 cmd.peekOrSeen(a.peek)
816 respField := cmd.sectionRespField(a)
818 if a.section.msgtext == nil && a.section.part == nil {
819 m := cmd.xensureMessage()
822 if a.partial != nil {
823 offset = min(int64(a.partial.offset), m.Size)
824 count = int64(a.partial.count)
825 if offset+count > m.Size {
826 count = m.Size - offset
829 return respField, readerSizeSyncliteral{&moxio.AtReader{R: msgr, Offset: offset}, count, false}
832 sr := cmd.xsection(a.section, part)
834 if a.partial != nil {
835 n, err := io.Copy(io.Discard, io.LimitReader(sr, int64(a.partial.offset)))
836 cmd.xcheckf(err, "skipping to offset for partial")
837 if n != int64(a.partial.offset) {
840 return respField, readerSyncliteral{io.LimitReader(sr, int64(a.partial.count))}
842 return respField, readerSyncliteral{sr}
845func (cmd *fetchCmd) xpartnumsDeref(nums []uint32, p *message.Part) *message.Part {
847 if (len(p.Parts) == 0 && p.Message == nil) && len(nums) == 1 && nums[0] == 1 {
852 for i, num := range nums {
853 index := int(num - 1)
854 if p.Message != nil {
855 err := p.SetMessageReaderAt()
856 cmd.xcheckf(err, "preparing submessage")
857 return cmd.xpartnumsDeref(nums[i:], p.Message)
859 if index < 0 || index >= len(p.Parts) {
860 cmd.xerrorf("requested part does not exist")
867func (cmd *fetchCmd) xsection(section *sectionSpec, p *message.Part) io.Reader {
868 // msgtext is not nil, i.e. HEADER* or TEXT (not MIME), for the top-level part (a message).
869 if section.part == nil {
870 return cmd.xsectionMsgtext(section.msgtext, p)
873 p = cmd.xpartnumsDeref(section.part.part, p)
875 // If there is no sectionMsgText, then this isn't for HEADER*, TEXT or MIME, i.e. a
876 // part body, e.g. "BODY[1]".
877 if section.part.text == nil {
881 // MIME is defined for all parts. Otherwise it's HEADER* or TEXT, which is only
883 if !section.part.text.mime {
884 if p.Message == nil {
885 cmd.xerrorf("part is not a message, cannot request header* or text")
888 err := p.SetMessageReaderAt()
889 cmd.xcheckf(err, "preparing submessage")
892 return cmd.xsectionMsgtext(section.part.text.msgtext, p)
896 h, err := io.ReadAll(p.HeaderReader())
897 cmd.xcheckf(err, "reading header")
899 matchesFields := func(line []byte) bool {
900 k := textproto.CanonicalMIMEHeaderKey(string(bytes.TrimRight(bytes.SplitN(line, []byte(":"), 2)[0], " \t")))
901 return strings.HasPrefix(k, "Content-")
905 hb := &bytes.Buffer{}
908 i := bytes.Index(line, []byte("\r\n"))
914 match = matchesFields(line) || match && (bytes.HasPrefix(line, []byte(" ")) || bytes.HasPrefix(line, []byte("\t")))
922func (cmd *fetchCmd) xsectionMsgtext(smt *sectionMsgtext, p *message.Part) io.Reader {
925 return p.HeaderReader()
927 case "HEADER.FIELDS":
928 return cmd.xmodifiedHeader(p, smt.headers, false)
930 case "HEADER.FIELDS.NOT":
931 return cmd.xmodifiedHeader(p, smt.headers, true)
934 // TEXT the body (excluding headers) of a message, either the top-level message, or
938 panic(serverError{fmt.Errorf("missing case")})
941func (cmd *fetchCmd) sectionRespField(a fetchAtt) string {
943 if len(a.sectionBinary) > 0 {
944 s += fmt.Sprintf("%d", a.sectionBinary[0])
945 for _, v := range a.sectionBinary[1:] {
946 s += "." + fmt.Sprintf("%d", v)
948 } else if a.section != nil {
949 if a.section.part != nil {
951 s += fmt.Sprintf("%d", p.part[0])
952 for _, v := range p.part[1:] {
953 s += "." + fmt.Sprintf("%d", v)
959 s += "." + cmd.sectionMsgtextName(p.text.msgtext)
962 } else if a.section.msgtext != nil {
963 s += cmd.sectionMsgtextName(a.section.msgtext)
968 if a.field != "BINARY" && a.partial != nil {
969 s += fmt.Sprintf("<%d>", a.partial.offset)
974func (cmd *fetchCmd) sectionMsgtextName(smt *sectionMsgtext) string {
976 if strings.HasPrefix(smt.s, "HEADER.FIELDS") {
978 for _, h := range smt.headers {
979 l = append(l, astring(h))
981 s += " " + l.pack(cmd.conn)
986func bodyFldParams(p *message.Part) token {
987 if len(p.ContentTypeParams) == 0 {
990 params := make(listspace, 0, 2*len(p.ContentTypeParams))
991 // Ensure same ordering, easier for testing.
992 for _, k := range slices.Sorted(maps.Keys(p.ContentTypeParams)) {
993 v := p.ContentTypeParams[k]
994 params = append(params, string0(strings.ToUpper(k)), string0(v))
999func bodyFldEnc(cte *string) token {
1004 up := strings.ToUpper(s)
1006 case "7BIT", "8BIT", "BINARY", "BASE64", "QUOTED-PRINTABLE":
1012func bodyFldMd5(p *message.Part) token {
1013 if p.ContentMD5 == nil {
1016 return string0(*p.ContentMD5)
1019func bodyFldDisp(log mlog.Log, p *message.Part) token {
1020 if p.ContentDisposition == nil {
1025 // mime.ParseMediaType recombines parameter value continuations like "title*0" and
1027 // And decodes character sets and removes language tags, like
1030 disp, params, err := mime.ParseMediaType(*p.ContentDisposition)
1032 log.Debugx("parsing content-disposition, ignoring", err, slog.String("header", *p.ContentDisposition))
1034 } else if len(params) == 0 {
1035 log.Debug("content-disposition has no parameters, ignoring", slog.String("header", *p.ContentDisposition))
1038 var fields listspace
1039 for _, k := range slices.Sorted(maps.Keys(params)) {
1040 fields = append(fields, string0(k), string0(params[k]))
1042 return listspace{string0(disp), fields}
1045func bodyFldLang(p *message.Part) token {
1047 if p.ContentLanguage == nil {
1051 for _, s := range strings.Split(*p.ContentLanguage, ",") {
1052 s = strings.TrimSpace(s)
1054 return string0(*p.ContentLanguage)
1056 l = append(l, string0(s))
1061func bodyFldLoc(p *message.Part) token {
1062 if p.ContentLocation == nil {
1065 return string0(*p.ContentLocation)
1068// xbodystructure returns a "body".
1069// calls itself for multipart messages and message/{rfc822,global}.
1070func xbodystructure(log mlog.Log, p *message.Part, extensible bool) token {
1071 if p.MediaType == "MULTIPART" {
1074 for i := range p.Parts {
1075 bodies = append(bodies, xbodystructure(log, &p.Parts[i], extensible))
1077 r := listspace{bodies, string0(p.MediaSubType)}
1082 bodyFldDisp(log, p),
1092 if p.MediaType == "TEXT" {
1098 nilOrString(p.ContentID),
1099 nilOrString(p.ContentDescription),
1100 bodyFldEnc(p.ContentTransferEncoding),
1101 number(p.EndOffset - p.BodyOffset),
1102 number(p.RawLineCount),
1104 } else if p.MediaType == "MESSAGE" && (p.MediaSubType == "RFC822" || p.MediaSubType == "GLOBAL") {
1106 // note: we don't have to prepare p.Message for reading, because we aren't going to read from it.
1111 nilOrString(p.ContentID),
1112 nilOrString(p.ContentDescription),
1113 bodyFldEnc(p.ContentTransferEncoding),
1114 number(p.EndOffset - p.BodyOffset),
1115 xenvelope(p.Message),
1116 xbodystructure(log, p.Message, extensible),
1117 number(p.RawLineCount), // todo: or mp.RawLineCount?
1121 switch p.MediaType {
1122 case "APPLICATION", "AUDIO", "IMAGE", "FONT", "MESSAGE", "MODEL", "VIDEO":
1123 media = dquote(p.MediaType)
1125 media = string0(p.MediaType)
1132 nilOrString(p.ContentID),
1133 nilOrString(p.ContentDescription),
1134 bodyFldEnc(p.ContentTransferEncoding),
1135 number(p.EndOffset - p.BodyOffset),
1142 bodyFldDisp(log, p),