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?
16 "github.com/mjl-/bstore"
18 "github.com/mjl-/mox/message"
19 "github.com/mjl-/mox/mox-"
20 "github.com/mjl-/mox/moxio"
21 "github.com/mjl-/mox/store"
25// functions to handle fetch attribute requests are defined on fetchCmd.
28 isUID bool // If this is a UID FETCH command.
29 rtx *bstore.Tx // Read-only transaction, kept open while processing all messages.
30 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.
31 hasChangedSince bool // Whether CHANGEDSINCE was set. Enables MODSEQ in response.
32 expungeIssued bool // Set if any message has been expunged. Can happen for expunged messages.
34 uid store.UID // UID currently processing.
37 needModseq bool // Whether untagged responses needs modseq.
38 newPreviews map[store.UID]string // Save with messages when done.
40 // Loaded when first needed, closed when message was processed.
41 m *store.Message // Message currently being processed.
46// error when processing an attribute. we typically just don't respond with requested attributes that encounter a failure.
47type attrError struct{ err error }
49func (e attrError) Error() string {
53// raise error processing an attribute.
54func (cmd *fetchCmd) xerrorf(format string, args ...any) {
55 panic(attrError{fmt.Errorf(format, args...)})
58func (cmd *fetchCmd) xcheckf(err error, format string, args ...any) {
60 msg := fmt.Sprintf(format, args...)
61 cmd.xerrorf("%s: %w", msg, err)
65// Fetch returns information about messages, be it email envelopes, headers,
66// bodies, full messages, flags.
69func (c *conn) cmdxFetch(isUID bool, tag, cmdstr string, p *parser) {
78 atts := p.xfetchAtts(isUID)
79 var changedSince int64
80 var haveChangedSince bool
86 seen := map[string]bool{}
89 if isUID && p.conn.enabled[capQresync] {
91 w = p.xtakelist("CHANGEDSINCE", "VANISHED")
93 w = p.xtakelist("CHANGEDSINCE")
96 xsyntaxErrorf("duplicate fetch modifier %s", w)
102 changedSince = p.xnumber64()
103 // workaround: ios mail (16.5.1) was seen sending changedSince 0 on an existing account that got condstore enabled.
104 if changedSince == 0 && mox.Pedantic {
106 xsyntaxErrorf("changedsince modseq must be > 0")
109 p.conn.xensureCondstore(nil)
110 haveChangedSince = true
121 if vanished && !haveChangedSince {
122 xsyntaxErrorf("VANISHED can only be used with CHANGEDSINCE")
127 // We only keep a wlock, only for initial checks and listing the uids. Then we
128 // unlock and work without a lock. So changes to the store can happen, and we need
129 // to deal with that. If we need to mark messages as seen, we do so after
130 // processing the fetch for all messages, in a single write transaction. We don't
131 // send untagged changes for those \seen flag changes before finishing this
132 // command, because we have to sequence all changes properly, and since we don't
133 // (want to) hold a wlock while processing messages (can be many!), other changes
134 // may have happened to the store. So instead, we'll silently mark messages as seen
135 // (the client should know this is happening anyway!), then broadcast the changes
136 // to everyone, including ourselves. A noop/idle command that may come next will
137 // return the \seen flag changes, in the correct order, with the correct modseq. We
138 // also cannot just apply pending changes while processing. It is not allowed at
139 // all for non-uid-fetch. It would also make life more complicated, e.g. we would
140 // perhaps have to check if newly added messages also match uid fetch set that was
144 var vanishedUIDs []store.UID
146 cmd := &fetchCmd{conn: c, isUID: isUID, hasChangedSince: haveChangedSince, newPreviews: map[store.UID]string{}}
152 err := cmd.rtx.Rollback()
153 c.log.Check(err, "rollback rtx")
157 c.account.WithRLock(func() {
159 cmd.rtx, err = c.account.DB.Begin(context.TODO(), false)
160 cmd.xcheckf(err, "begin transaction")
162 // Ensure the mailbox still exists.
163 c.xmailboxID(cmd.rtx, c.mailboxID)
165 // With changedSince, the client is likely asking for a small set of changes. Use a
166 // database query to trim down the uids we need to look at.
168 if changedSince > 0 {
169 q := bstore.QueryTx[store.Message](cmd.rtx)
170 q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
171 q.FilterGreater("ModSeq", store.ModSeqFromClient(changedSince))
173 q.FilterEqual("Expunged", false)
175 err := q.ForEach(func(m store.Message) error {
177 vanishedUIDs = append(vanishedUIDs, m.UID)
179 if nums.containsUID(m.UID, c.uids, c.searchResult) {
180 uids = append(uids, m.UID)
183 seq := c.sequence(m.UID)
184 if seq > 0 && nums.containsSeq(seq, c.uids, c.searchResult) {
185 uids = append(uids, m.UID)
190 xcheckf(err, "looking up messages with changedsince")
192 uids = c.xnumSetUIDs(isUID, nums)
200 delModSeq, err := c.account.HighestDeletedModSeq(cmd.rtx)
201 xcheckf(err, "looking up highest deleted modseq")
202 if changedSince >= delModSeq.Client() {
206 // First sort the uids we already found, for fast lookup.
207 slices.Sort(vanishedUIDs)
209 // We'll be gathering any more vanished uids in more.
210 more := map[store.UID]struct{}{}
211 checkVanished := func(uid store.UID) {
212 if uidSearch(c.uids, uid) <= 0 && uidSearch(vanishedUIDs, uid) <= 0 {
213 more[uid] = struct{}{}
216 // Now look through the requested uids. We may have a searchResult, handle it
217 // separately from a numset with potential stars, over which we can more easily
219 if nums.searchResult {
220 for _, uid := range c.searchResult {
224 iter := nums.interpretStar(c.uids).newIter()
226 num, ok := iter.Next()
230 checkVanished(store.UID(num))
233 vanishedUIDs = slices.AppendSeq(vanishedUIDs, maps.Keys(more))
235 // We are continuing without a lock, working off our snapshot of uids to process.
238 if len(vanishedUIDs) > 0 {
239 // Mention all vanished UIDs in compact numset form.
241 slices.Sort(vanishedUIDs)
242 // No hard limit on response sizes, but clients are recommended to not send more
243 // than 8k. We send a more conservative max 4k.
244 for _, s := range compactUIDSet(vanishedUIDs).Strings(4*1024 - 32) {
245 c.bwritelinef("* VANISHED (EARLIER) %s", s)
249 for _, cmd.uid = range uids {
250 cmd.conn.log.Debug("processing uid", slog.Any("uid", cmd.uid))
254 // We've returned all data. Now we mark messages as seen in one go, in a new write
255 // transaction. We don't send untagged messages for the changes, since there may be
256 // unprocessed pending changes. Instead, we broadcast them to ourselve too, so a
257 // next noop/idle will return the flags to the client.
259 err := cmd.rtx.Rollback()
260 c.log.Check(err, "fetch read tx rollback")
264 // command, in a single transaction.
265 if len(cmd.updateSeen) > 0 || len(cmd.newPreviews) > 0 {
266 c.account.WithWLock(func() {
267 changes := make([]store.Change, 0, len(cmd.updateSeen)+1)
269 c.xdbwrite(func(wtx *bstore.Tx) {
270 mb, err := store.MailboxID(wtx, c.mailboxID)
271 if err == store.ErrMailboxExpunged {
272 xusercodeErrorf("NONEXISTENT", "mailbox has been expunged")
274 xcheckf(err, "get mailbox for updating counts after marking as seen")
276 var modseq store.ModSeq
278 for _, uid := range cmd.updateSeen {
279 m, err := bstore.QueryTx[store.Message](wtx).FilterNonzero(store.Message{MailboxID: c.mailboxID, UID: uid}).Get()
280 xcheckf(err, "get message")
282 // Message has been deleted in the mean time.
283 cmd.expungeIssued = true
287 // Message already marked as seen by another process.
292 modseq, err = c.account.NextModSeq(wtx)
293 xcheckf(err, "get next mod seq")
297 mb.Sub(m.MailboxCounts())
299 mb.Add(m.MailboxCounts())
300 changes = append(changes, m.ChangeFlags(oldFlags))
304 xcheckf(err, "mark message as seen")
307 changes = append(changes, mb.ChangeCounts())
309 for uid, s := range cmd.newPreviews {
310 m, err := bstore.QueryTx[store.Message](wtx).FilterNonzero(store.Message{MailboxID: c.mailboxID, UID: uid}).Get()
311 xcheckf(err, "get message")
313 // Message has been deleted in the mean time.
314 cmd.expungeIssued = true
318 // note: we are not updating modseq.
322 xcheckf(err, "saving preview with message")
327 err = wtx.Update(&mb)
328 xcheckf(err, "update mailbox with counts and modseq")
332 // Broadcast these changes also to ourselves, so we'll send the updated flags, but
333 // in the correct order, after other changes.
334 store.BroadcastChanges(c.account, changes)
338 if cmd.expungeIssued {
341 c.writeresultf("%s OK [EXPUNGEISSUED] at least one message was expunged", tag)
347func (cmd *fetchCmd) xensureMessage() *store.Message {
352 // We do not filter by Expunged, the message may have been deleted in other
353 // sessions, but not in ours.
354 q := bstore.QueryTx[store.Message](cmd.rtx)
355 q.FilterNonzero(store.Message{MailboxID: cmd.conn.mailboxID, UID: cmd.uid})
357 cmd.xcheckf(err, "get message for uid %d", cmd.uid)
360 cmd.expungeIssued = true
365func (cmd *fetchCmd) xensureParsed() (*store.MsgReader, *message.Part) {
367 return cmd.msgr, cmd.part
370 m := cmd.xensureMessage()
372 cmd.msgr = cmd.conn.account.MessageReader(*m)
375 err := cmd.msgr.Close()
376 cmd.conn.xsanity(err, "closing messagereader")
381 p, err := m.LoadPart(cmd.msgr)
382 xcheckf(err, "load parsed message")
384 return cmd.msgr, cmd.part
387func (cmd *fetchCmd) process(atts []fetchAtt) {
392 err := cmd.msgr.Close()
393 cmd.conn.xsanity(err, "closing messagereader")
401 err, ok := x.(attrError)
405 cmd.conn.log.Infox("processing fetch attribute", err, slog.Any("uid", cmd.uid))
406 xuserErrorf("processing fetch attribute: %v", err)
409 data := listspace{bare("UID"), number(cmd.uid)}
412 cmd.needFlags = false
413 cmd.needModseq = false
415 for _, a := range atts {
416 data = append(data, cmd.xprocessAtt(a)...)
420 cmd.updateSeen = append(cmd.updateSeen, cmd.uid)
424 m := cmd.xensureMessage()
425 data = append(data, bare("FLAGS"), flaglist(m.Flags, m.Keywords))
428 // The wording around when to include the MODSEQ attribute is hard to follow and is
429 // specified and refined in several places.
431 // An additional rule applies to "QRESYNC servers" (we'll assume it only applies
432 // when QRESYNC is enabled on a connection): setting the \Seen flag also triggers
436 // subsequent untagged fetch responses", then lists cases, but leaves out FETCH/UID
437 // FETCH. That appears intentional, it is not a list of examples, it is the full
438 // list, and the "all subsequent untagged fetch responses" doesn't mean "all", just
439 // those covering the listed cases. That makes sense, because otherwise all the
440 // other mentioning of cases elsewhere in the RFC would be too superfluous.
443 if cmd.needModseq || cmd.hasChangedSince || cmd.conn.enabled[capQresync] && cmd.isUID {
444 m := cmd.xensureMessage()
445 data = append(data, bare("MODSEQ"), listspace{bare(fmt.Sprintf("%d", m.ModSeq.Client()))})
448 // Write errors are turned into panics because we write through c.
449 fmt.Fprintf(cmd.conn.xbw, "* %d FETCH ", cmd.conn.xsequence(cmd.uid))
450 data.writeTo(cmd.conn, cmd.conn.xbw)
451 cmd.conn.xbw.Write([]byte("\r\n"))
454// result for one attribute. if processing fails, e.g. because data was requested
455// that doesn't exist and cannot be represented in imap, the attribute is simply
456// not returned to the user. in this case, the returned value is a nil list.
457func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token {
464 _, part := cmd.xensureParsed()
465 envelope := xenvelope(part)
466 return []token{bare("ENVELOPE"), envelope}
470 m := cmd.xensureMessage()
471 return []token{bare("INTERNALDATE"), dquote(m.Received.Format("_2-Jan-2006 15:04:05 -0700"))}
474 m := cmd.xensureMessage()
475 // For messages in storage from before we implemented this extension, we don't have
476 // a savedate, and we return nil. This is normally meant to be per mailbox, but
478 var savedate token = nilt
479 if m.SaveDate != nil {
480 savedate = dquote(m.SaveDate.Format("_2-Jan-2006 15:04:05 -0700"))
482 return []token{bare("SAVEDATE"), savedate}
484 case "BODYSTRUCTURE":
485 _, part := cmd.xensureParsed()
486 bs := xbodystructure(part, true)
487 return []token{bare("BODYSTRUCTURE"), bs}
490 respField, t := cmd.xbody(a)
494 return []token{bare(respField), t}
497 _, p := cmd.xensureParsed()
498 if len(a.sectionBinary) == 0 {
499 // Must return the size of the entire message but with decoded body.
500 // todo: make this less expensive and/or cache the result?
501 n, err := io.Copy(io.Discard, cmd.xbinaryMessageReader(p))
502 cmd.xcheckf(err, "reading message as binary for its size")
503 return []token{bare(cmd.sectionRespField(a)), number(uint32(n))}
505 p = cmd.xpartnumsDeref(a.sectionBinary, p)
506 if len(p.Parts) > 0 || p.Message != nil {
508 cmd.xerrorf("binary only allowed on leaf parts, not multipart/* or message/rfc822 or message/global")
510 return []token{bare(cmd.sectionRespField(a)), number(p.DecodedSize)}
513 respField, t := cmd.xbinary(a)
517 return []token{bare(respField), t}
520 m := cmd.xensureMessage()
521 return []token{bare("RFC822.SIZE"), number(m.Size)}
523 case "RFC822.HEADER":
527 section: §ionSpec{
528 msgtext: §ionMsgtext{s: "HEADER"},
531 respField, t := cmd.xbody(ba)
535 return []token{bare(a.field), t}
540 section: §ionSpec{},
542 respField, t := cmd.xbody(ba)
546 return []token{bare(a.field), t}
551 section: §ionSpec{
552 msgtext: §ionMsgtext{s: "TEXT"},
555 respField, t := cmd.xbody(ba)
559 return []token{bare(a.field), t}
565 cmd.needModseq = true
568 m := cmd.xensureMessage()
570 // We ignore "lazy", generating the preview is fast enough.
572 // Get the preview. We'll save all generated previews in a single transaction at
574 _, p := cmd.xensureParsed()
575 s, err := p.Preview(cmd.conn.log)
576 cmd.xcheckf(err, "generating preview")
578 cmd.newPreviews[m.UID] = s
593 s = strings.TrimSpace(s)
596 return []token{bare(a.field), t}
599 xserverErrorf("field %q not yet implemented", a.field)
605func xenvelope(p *message.Part) token {
606 var env message.Envelope
607 if p.Envelope != nil {
610 var date token = nilt
611 if !env.Date.IsZero() {
613 date = string0(env.Date.Format("Mon, 2 Jan 2006 15:04:05 -0700"))
615 var subject token = nilt
616 if env.Subject != "" {
617 subject = string0(env.Subject)
619 var inReplyTo token = nilt
620 if env.InReplyTo != "" {
621 inReplyTo = string0(env.InReplyTo)
623 var messageID token = nilt
624 if env.MessageID != "" {
625 messageID = string0(env.MessageID)
628 addresses := func(l []message.Address) token {
633 for _, a := range l {
634 var name token = nilt
636 name = string0(a.Name)
638 user := string0(a.User)
639 var host token = nilt
641 host = string0(a.Host)
643 r = append(r, listspace{name, nilt, user, host})
650 if len(sender) == 0 {
653 replyTo := env.ReplyTo
654 if len(replyTo) == 0 {
672func (cmd *fetchCmd) peekOrSeen(peek bool) {
673 if cmd.conn.readonly || peek {
676 m := cmd.xensureMessage()
683// reader that returns the message, but with header Content-Transfer-Encoding left out.
684func (cmd *fetchCmd) xbinaryMessageReader(p *message.Part) io.Reader {
685 hr := cmd.xmodifiedHeader(p, []string{"Content-Transfer-Encoding"}, true)
686 return io.MultiReader(hr, p.Reader())
689// return header with only fields, or with everything except fields if "not" is set.
690func (cmd *fetchCmd) xmodifiedHeader(p *message.Part, fields []string, not bool) io.Reader {
691 h, err := io.ReadAll(p.HeaderReader())
692 cmd.xcheckf(err, "reading header")
694 matchesFields := func(line []byte) bool {
695 k := bytes.TrimRight(bytes.SplitN(line, []byte(":"), 2)[0], " \t")
696 for _, f := range fields {
697 if bytes.EqualFold(k, []byte(f)) {
705 hb := &bytes.Buffer{}
708 i := bytes.Index(line, []byte("\r\n"))
714 match = matchesFields(line) || match && (bytes.HasPrefix(line, []byte(" ")) || bytes.HasPrefix(line, []byte("\t")))
715 if match != not || len(line) == 2 {
722func (cmd *fetchCmd) xbinary(a fetchAtt) (string, token) {
723 _, part := cmd.xensureParsed()
725 cmd.peekOrSeen(a.peek)
726 if len(a.sectionBinary) == 0 {
727 r := cmd.xbinaryMessageReader(part)
728 if a.partial != nil {
729 r = cmd.xpartialReader(a.partial, r)
731 return cmd.sectionRespField(a), readerSyncliteral{r}
735 if len(a.sectionBinary) > 0 {
736 p = cmd.xpartnumsDeref(a.sectionBinary, p)
738 if len(p.Parts) != 0 || p.Message != nil {
740 cmd.xerrorf("binary only allowed on leaf parts, not multipart/* or message/rfc822 or message/global")
743 switch p.ContentTransferEncoding {
744 case "", "7BIT", "8BIT", "BINARY", "BASE64", "QUOTED-PRINTABLE":
747 xusercodeErrorf("UNKNOWN-CTE", "unknown Content-Transfer-Encoding %q", p.ContentTransferEncoding)
751 if a.partial != nil {
752 r = cmd.xpartialReader(a.partial, r)
754 return cmd.sectionRespField(a), readerSyncliteral{r}
757func (cmd *fetchCmd) xpartialReader(partial *partial, r io.Reader) io.Reader {
758 n, err := io.Copy(io.Discard, io.LimitReader(r, int64(partial.offset)))
759 cmd.xcheckf(err, "skipping to offset for partial")
760 if n != int64(partial.offset) {
763 return io.LimitReader(r, int64(partial.count))
766func (cmd *fetchCmd) xbody(a fetchAtt) (string, token) {
767 msgr, part := cmd.xensureParsed()
769 if a.section == nil {
770 // Non-extensible form of BODYSTRUCTURE.
771 return a.field, xbodystructure(part, false)
774 cmd.peekOrSeen(a.peek)
776 respField := cmd.sectionRespField(a)
778 if a.section.msgtext == nil && a.section.part == nil {
779 m := cmd.xensureMessage()
782 if a.partial != nil {
783 offset = min(int64(a.partial.offset), m.Size)
784 count = int64(a.partial.count)
785 if offset+count > m.Size {
786 count = m.Size - offset
789 return respField, readerSizeSyncliteral{&moxio.AtReader{R: msgr, Offset: offset}, count, false}
792 sr := cmd.xsection(a.section, part)
794 if a.partial != nil {
795 n, err := io.Copy(io.Discard, io.LimitReader(sr, int64(a.partial.offset)))
796 cmd.xcheckf(err, "skipping to offset for partial")
797 if n != int64(a.partial.offset) {
800 return respField, readerSyncliteral{io.LimitReader(sr, int64(a.partial.count))}
802 return respField, readerSyncliteral{sr}
805func (cmd *fetchCmd) xpartnumsDeref(nums []uint32, p *message.Part) *message.Part {
807 if (len(p.Parts) == 0 && p.Message == nil) && len(nums) == 1 && nums[0] == 1 {
812 for i, num := range nums {
813 index := int(num - 1)
814 if p.Message != nil {
815 err := p.SetMessageReaderAt()
816 cmd.xcheckf(err, "preparing submessage")
817 return cmd.xpartnumsDeref(nums[i:], p.Message)
819 if index < 0 || index >= len(p.Parts) {
820 cmd.xerrorf("requested part does not exist")
827func (cmd *fetchCmd) xsection(section *sectionSpec, p *message.Part) io.Reader {
828 if section.part == nil {
829 return cmd.xsectionMsgtext(section.msgtext, p)
832 p = cmd.xpartnumsDeref(section.part.part, p)
834 if section.part.text == nil {
839 if p.Message != nil {
840 err := p.SetMessageReaderAt()
841 cmd.xcheckf(err, "preparing submessage")
845 if !section.part.text.mime {
846 return cmd.xsectionMsgtext(section.part.text.msgtext, p)
850 h, err := io.ReadAll(p.HeaderReader())
851 cmd.xcheckf(err, "reading header")
853 matchesFields := func(line []byte) bool {
854 k := textproto.CanonicalMIMEHeaderKey(string(bytes.TrimRight(bytes.SplitN(line, []byte(":"), 2)[0], " \t")))
856 return (p.Envelope != nil && k == "Mime-Version") || strings.HasPrefix(k, "Content-")
860 hb := &bytes.Buffer{}
863 i := bytes.Index(line, []byte("\r\n"))
869 match = matchesFields(line) || match && (bytes.HasPrefix(line, []byte(" ")) || bytes.HasPrefix(line, []byte("\t")))
870 if match || len(line) == 2 {
877func (cmd *fetchCmd) xsectionMsgtext(smt *sectionMsgtext, p *message.Part) io.Reader {
878 if smt.s == "HEADER" {
879 return p.HeaderReader()
883 case "HEADER.FIELDS":
884 return cmd.xmodifiedHeader(p, smt.headers, false)
886 case "HEADER.FIELDS.NOT":
887 return cmd.xmodifiedHeader(p, smt.headers, true)
890 // It appears imap clients expect to get the body of the message, not a "text body"
894 panic(serverError{fmt.Errorf("missing case")})
897func (cmd *fetchCmd) sectionRespField(a fetchAtt) string {
899 if len(a.sectionBinary) > 0 {
900 s += fmt.Sprintf("%d", a.sectionBinary[0])
901 for _, v := range a.sectionBinary[1:] {
902 s += "." + fmt.Sprintf("%d", v)
904 } else if a.section != nil {
905 if a.section.part != nil {
907 s += fmt.Sprintf("%d", p.part[0])
908 for _, v := range p.part[1:] {
909 s += "." + fmt.Sprintf("%d", v)
915 s += "." + cmd.sectionMsgtextName(p.text.msgtext)
918 } else if a.section.msgtext != nil {
919 s += cmd.sectionMsgtextName(a.section.msgtext)
924 if a.field != "BINARY" && a.partial != nil {
925 s += fmt.Sprintf("<%d>", a.partial.offset)
930func (cmd *fetchCmd) sectionMsgtextName(smt *sectionMsgtext) string {
932 if strings.HasPrefix(smt.s, "HEADER.FIELDS") {
934 for _, h := range smt.headers {
935 l = append(l, astring(h))
937 s += " " + l.pack(cmd.conn)
942func bodyFldParams(params map[string]string) token {
943 if len(params) == 0 {
946 // Ensure same ordering, easier for testing.
948 for k := range params {
949 keys = append(keys, k)
952 l := make(listspace, 2*len(keys))
954 for _, k := range keys {
955 l[i] = string0(strings.ToUpper(k))
956 l[i+1] = string0(params[k])
962func bodyFldEnc(s string) token {
963 up := strings.ToUpper(s)
965 case "7BIT", "8BIT", "BINARY", "BASE64", "QUOTED-PRINTABLE":
971// xbodystructure returns a "body".
972// calls itself for multipart messages and message/{rfc822,global}.
973func xbodystructure(p *message.Part, extensible bool) token {
974 if p.MediaType == "MULTIPART" {
977 for i := range p.Parts {
978 bodies = append(bodies, xbodystructure(&p.Parts[i], extensible))
980 r := listspace{bodies, string0(p.MediaSubType)}
982 if len(p.ContentTypeParams) == 0 {
985 params := make(listspace, 0, 2*len(p.ContentTypeParams))
986 for k, v := range p.ContentTypeParams {
987 params = append(params, string0(k), string0(v))
989 r = append(r, params)
997 if p.MediaType == "TEXT" {
1003 nilOrString(p.ContentID),
1004 nilOrString(p.ContentDescription),
1005 bodyFldEnc(p.ContentTransferEncoding),
1006 number(p.EndOffset - p.BodyOffset),
1007 number(p.RawLineCount),
1009 } else if p.MediaType == "MESSAGE" && (p.MediaSubType == "RFC822" || p.MediaSubType == "GLOBAL") {
1011 // note: we don't have to prepare p.Message for reading, because we aren't going to read from it.
1016 nilOrString(p.ContentID),
1017 nilOrString(p.ContentDescription),
1018 bodyFldEnc(p.ContentTransferEncoding),
1019 number(p.EndOffset - p.BodyOffset),
1020 xenvelope(p.Message),
1021 xbodystructure(p.Message, extensible),
1022 number(p.RawLineCount), // todo: or mp.RawLineCount?
1026 switch p.MediaType {
1027 case "APPLICATION", "AUDIO", "IMAGE", "FONT", "MESSAGE", "MODEL", "VIDEO":
1028 media = dquote(p.MediaType)
1030 media = string0(p.MediaType)
1037 nilOrString(p.ContentID),
1038 nilOrString(p.ContentDescription),
1039 bodyFldEnc(p.ContentTransferEncoding),
1040 number(p.EndOffset - p.BodyOffset),
1043 // todo: if "extensible", we could add the value of the "content-md5" header. we don't have it in our parsed data structure, so we don't add it. likely no one would use it, also not any of the other optional fields.
../rfc/9051:6366