From 2defbce0bc05d93c6c4c63f1d2f8d545879d311b Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Sat, 5 Apr 2025 15:46:17 +0200 Subject: [PATCH] imapserver: return all the extensible fields for bodystructure, notably for content-disposition 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. --- imapserver/fetch.go | 124 +++++++++++++++++++++++++++----------- imapserver/fetch_test.go | 43 +++++++------ imapserver/server_test.go | 4 ++ message/part.go | 25 ++++++-- rfc/index.txt | 4 ++ store/account.go | 6 +- webmail/api.json | 30 ++++++++- webmail/api.ts | 8 ++- webmail/msg.js | 2 +- webmail/text.js | 2 +- webmail/webmail.js | 2 +- 11 files changed, 185 insertions(+), 65 deletions(-) diff --git a/imapserver/fetch.go b/imapserver/fetch.go index 38029b4..4171aef 100644 --- a/imapserver/fetch.go +++ b/imapserver/fetch.go @@ -9,17 +9,18 @@ import ( "io" "log/slog" "maps" + "mime" "net/textproto" - "sort" + "slices" "strings" "github.com/mjl-/bstore" "github.com/mjl-/mox/message" + "github.com/mjl-/mox/mlog" "github.com/mjl-/mox/mox-" "github.com/mjl-/mox/moxio" "github.com/mjl-/mox/store" - "slices" ) // functions to handle fetch attribute requests are defined on fetchCmd. @@ -483,7 +484,7 @@ func (cmd *fetchCmd) xprocessAtt(a fetchAtt) []token { case "BODYSTRUCTURE": _, part := cmd.xensureParsed() - bs := xbodystructure(part, true) + bs := xbodystructure(cmd.conn.log, part, true) return []token{bare("BODYSTRUCTURE"), bs} case "BODY": @@ -768,7 +769,7 @@ func (cmd *fetchCmd) xbody(a fetchAtt) (string, token) { if a.section == nil { // Non-extensible form of BODYSTRUCTURE. - return a.field, xbodystructure(part, false) + return a.field, xbodystructure(cmd.conn.log, part, false) } cmd.peekOrSeen(a.peek) @@ -939,24 +940,17 @@ func (cmd *fetchCmd) sectionMsgtextName(smt *sectionMsgtext) string { return s } -func bodyFldParams(params map[string]string) token { - if len(params) == 0 { +func bodyFldParams(p *message.Part) token { + if len(p.ContentTypeParams) == 0 { return nilt } + params := make(listspace, 0, 2*len(p.ContentTypeParams)) // Ensure same ordering, easier for testing. - var keys []string - for k := range params { - keys = append(keys, k) + for _, k := range slices.Sorted(maps.Keys(p.ContentTypeParams)) { + v := p.ContentTypeParams[k] + params = append(params, string0(strings.ToUpper(k)), string0(v)) } - sort.Strings(keys) - l := make(listspace, 2*len(keys)) - i := 0 - for _, k := range keys { - l[i] = string0(strings.ToUpper(k)) - l[i+1] = string0(params[k]) - i += 2 - } - return l + return params } func bodyFldEnc(s string) token { @@ -968,26 +962,80 @@ func bodyFldEnc(s string) token { return string0(s) } +func bodyFldMd5(p *message.Part) token { + if p.ContentMD5 == "" { + return nilt + } + return string0(p.ContentMD5) +} + +func bodyFldDisp(log mlog.Log, p *message.Part) token { + if p.ContentDisposition == "" { + return nilt + } + + // ../rfc/9051:5989 + // mime.ParseMediaType recombines parameter value continuations like "title*0" and + // "title*1" into "title". ../rfc/2231:147 + // And decodes character sets and removes language tags, like + // "title*0*=us-ascii'en'hello%20world. ../rfc/2231:210 + + disp, params, err := mime.ParseMediaType(p.ContentDisposition) + if err != nil { + log.Debugx("parsing content-disposition, ignoring", err, slog.String("header", p.ContentDisposition)) + return nilt + } else if len(params) == 0 { + log.Debug("content-disposition has no parameters, ignoring", slog.String("header", p.ContentDisposition)) + return nilt + } + var fields listspace + for _, k := range slices.Sorted(maps.Keys(params)) { + fields = append(fields, string0(k), string0(params[k])) + } + return listspace{string0(disp), fields} +} + +func bodyFldLang(p *message.Part) token { + // todo: ../rfc/3282:86 ../rfc/5646:218 we currently just split on comma and trim space, should properly parse header. + if p.ContentLanguage == "" { + return nilt + } + var l listspace + for _, s := range strings.Split(p.ContentLanguage, ",") { + s = strings.TrimSpace(s) + if s == "" { + return string0(p.ContentLanguage) + } + l = append(l, string0(s)) + } + return l +} + +func bodyFldLoc(p *message.Part) token { + if p.ContentLocation == "" { + return nilt + } + return string0(p.ContentLocation) +} + // xbodystructure returns a "body". // calls itself for multipart messages and message/{rfc822,global}. -func xbodystructure(p *message.Part, extensible bool) token { +func xbodystructure(log mlog.Log, p *message.Part, extensible bool) token { if p.MediaType == "MULTIPART" { // Multipart, ../rfc/9051:6355 ../rfc/9051:6411 var bodies concat for i := range p.Parts { - bodies = append(bodies, xbodystructure(&p.Parts[i], extensible)) + bodies = append(bodies, xbodystructure(log, &p.Parts[i], extensible)) } r := listspace{bodies, string0(p.MediaSubType)} + // ../rfc/9051:6371 if extensible { - if len(p.ContentTypeParams) == 0 { - r = append(r, nilt) - } else { - params := make(listspace, 0, 2*len(p.ContentTypeParams)) - for k, v := range p.ContentTypeParams { - params = append(params, string0(k), string0(v)) - } - r = append(r, params) - } + r = append(r, + bodyFldParams(p), + bodyFldDisp(log, p), + bodyFldLang(p), + bodyFldLoc(p), + ) } return r } @@ -999,7 +1047,7 @@ func xbodystructure(p *message.Part, extensible bool) token { r = listspace{ dquote("TEXT"), string0(p.MediaSubType), // ../rfc/9051:6739 // ../rfc/9051:6376 - bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401 + bodyFldParams(p), // ../rfc/9051:6401 nilOrString(p.ContentID), nilOrString(p.ContentDescription), bodyFldEnc(p.ContentTransferEncoding), @@ -1012,13 +1060,13 @@ func xbodystructure(p *message.Part, extensible bool) token { r = listspace{ dquote("MESSAGE"), dquote(p.MediaSubType), // ../rfc/9051:6732 // ../rfc/9051:6376 - bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401 + bodyFldParams(p), // ../rfc/9051:6401 nilOrString(p.ContentID), nilOrString(p.ContentDescription), bodyFldEnc(p.ContentTransferEncoding), number(p.EndOffset - p.BodyOffset), xenvelope(p.Message), - xbodystructure(p.Message, extensible), + xbodystructure(log, p.Message, extensible), number(p.RawLineCount), // todo: or mp.RawLineCount? } } else { @@ -1033,13 +1081,21 @@ func xbodystructure(p *message.Part, extensible bool) token { r = listspace{ media, string0(p.MediaSubType), // ../rfc/9051:6723 // ../rfc/9051:6376 - bodyFldParams(p.ContentTypeParams), // ../rfc/9051:6401 + bodyFldParams(p), // ../rfc/9051:6401 nilOrString(p.ContentID), nilOrString(p.ContentDescription), bodyFldEnc(p.ContentTransferEncoding), number(p.EndOffset - p.BodyOffset), } } - // 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 + if extensible { + // ../rfc/9051:6366 + r = append(r, + bodyFldMd5(p), + bodyFldDisp(log, p), + bodyFldLang(p), + bodyFldLoc(p), + ) + } return r } diff --git a/imapserver/fetch_test.go b/imapserver/fetch_test.go index 88eadbf..ad84c43 100644 --- a/imapserver/fetch_test.go +++ b/imapserver/fetch_test.go @@ -35,20 +35,23 @@ func TestFetch(t *testing.T) { MessageID: "", } 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: imapclient.BodyTypeText{ - MediaType: "TEXT", - MediaSubtype: "PLAIN", - BodyFields: imapclient.BodyFields{ - Params: [][2]string{[...]string{"CHARSET", "US-ASCII"}}, - Octets: 57, - }, - Lines: 2, - }, + 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" @@ -288,17 +291,17 @@ func TestFetch(t *testing.T) { RespAttr: "BODYSTRUCTURE", Body: imapclient.BodyTypeMpart{ Bodies: []any{ - imapclient.BodyTypeBasic{BodyFields: imapclient.BodyFields{Octets: 275}}, - imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "US-ASCII"}}, Octets: 114}, Lines: 3}, + 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}}, - imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}}, + 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"}}}, + Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"BOUNDARY", "unique-boundary-2"}}}, }, - imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5}, + imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5, Ext: &imapclient.BodyExtension1Part{}}, imapclient.BodyTypeMsg{ MediaType: "MESSAGE", MediaSubtype: "RFC822", @@ -311,12 +314,17 @@ func TestFetch(t *testing.T) { 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}, + 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"}}}, + Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"BOUNDARY", "unique-boundary-1"}}}, }, } tc.client.Append("inbox", makeAppendTime(nestedMessage, received)) @@ -390,6 +398,7 @@ aGVsbG8NCndvcmxkDQo= --unique-boundary-2 Content-Type: image/jpeg Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename=image.jpg --unique-boundary-2-- diff --git a/imapserver/server_test.go b/imapserver/server_test.go index 794e681..951d656 100644 --- a/imapserver/server_test.go +++ b/imapserver/server_test.go @@ -118,6 +118,7 @@ aGVsbG8NCndvcmxkDQo= --unique-boundary-2 Content-Type: image/jpeg Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename=image.jpg --unique-boundary-2-- @@ -133,6 +134,9 @@ Isn't it --unique-boundary-1 Content-Type: message/rfc822 +Content-MD5: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= +Content-Language: en,de +Content-Location: http://localhost From: info@mox.example To: mox diff --git a/message/part.go b/message/part.go index b954abf..f05920b 100644 --- a/message/part.go +++ b/message/part.go @@ -67,7 +67,11 @@ type Part struct { ContentTypeParams map[string]string // E.g. holds "boundary" for multipart messages. Has lower-case keys, and original case values. ContentID string ContentDescription string - ContentTransferEncoding string // In upper case. + ContentTransferEncoding string // In upper case. + ContentDisposition string + ContentMD5 string + ContentLanguage string + ContentLocation string Envelope *Envelope // Email message headers. Not for non-message parts. Parts []Part // Parts if this is a multipart. @@ -155,6 +159,10 @@ func fallbackPart(p Part, r io.ReaderAt, size int64) (Part, error) { ContentID: p.ContentID, ContentDescription: p.ContentDescription, ContentTransferEncoding: p.ContentTransferEncoding, + ContentDisposition: p.ContentDisposition, + ContentMD5: p.ContentMD5, + ContentLanguage: p.ContentLanguage, + ContentLocation: p.ContentLocation, Envelope: p.Envelope, // We don't keep: // - BoundaryOffset: irrelevant for top-level message. @@ -357,6 +365,10 @@ func newPart(log mlog.Log, strict bool, r io.ReaderAt, offset int64, parent *Par p.ContentID = p.header.Get("Content-Id") p.ContentDescription = p.header.Get("Content-Description") p.ContentTransferEncoding = strings.ToUpper(p.header.Get("Content-Transfer-Encoding")) + p.ContentDisposition = p.header.Get("Content-Disposition") + p.ContentMD5 = p.header.Get("Content-Md5") + p.ContentLanguage = p.header.Get("Content-Language") + p.ContentLocation = p.header.Get("Content-Location") if parent == nil { p.Envelope, err = parseEnvelope(log, mail.Header(p.header)) @@ -644,13 +656,16 @@ var ErrParamEncoding = errors.New("bad header parameter encoding") // If the returned error is an ErrParamEncoding, it can be treated as a diagnostic // and a filename may still be returned. func (p *Part) DispositionFilename() (disposition string, filename string, err error) { - h, err := p.Header() - if err != nil { - return "", "", fmt.Errorf("parsing header: %w", err) + cd := p.ContentDisposition + if cd == "" { + h, err := p.Header() + if err != nil { + return "", "", fmt.Errorf("parsing header: %w", err) + } + cd = h.Get("Content-Disposition") } var disp string var params map[string]string - cd := h.Get("Content-Disposition") if cd != "" { disp, params, err = mime.ParseMediaType(cd) } diff --git a/rfc/index.txt b/rfc/index.txt index f035158..c6d88e4 100644 --- a/rfc/index.txt +++ b/rfc/index.txt @@ -23,6 +23,7 @@ Also see IANA assignments, https://www.iana.org/protocols # Internet Message Format 822 Yes Obs Standard for ARPA Internet Text Messages 1847 No - Security Multiparts for MIME: Multipart/Signed and Multipart/Encrypted +1864 - - The Content-MD5 Header Field 2045 Yes - Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies 2046 Yes - Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types 2047 Yes - MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text @@ -30,12 +31,15 @@ Also see IANA assignments, https://www.iana.org/protocols 2076 - - Common Internet Message Headers 2183 Yes - Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field 2231 Yes - MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations +2557 - - MIME Encapsulation of Aggregate Documents, such as HTML (MHTML) +3282 - - Content Language Headers 3629 - - UTF-8, a transformation format of ISO 10646 3676 No - The Text/Plain Format and DelSp Parameters 4155 - - The application/mbox Media Type 5234 - - Augmented BNF for Syntax Specifications: ABNF 5322 Yes - Internet Message Format 5598 - - Internet Mail Architecture +5646 - - Tags for Identifying Languages 6854 - - Update to Internet Message Format to Allow Group Syntax in the "From:" and "Sender:" Header Fields 7405 - - Case-Sensitive String Support in ABNF 9078 - - Reaction: Indicating Summary Reaction to a Message diff --git a/store/account.go b/store/account.go index 09dc006..307e049 100644 --- a/store/account.go +++ b/store/account.go @@ -573,9 +573,9 @@ type Message struct { // want to strip whitespace. Preview *string - // ParsedBuf message structure. Currently saved as JSON of message.Part because bstore - // cannot yet store recursive types. Created when first needed, and saved in the - // database. + // ParsedBuf message structure. Currently saved as JSON of message.Part because + // bstore wasn't able to store recursive types when this was implemented. Created + // when first needed, and saved in the database. // todo: once replaced with non-json storage, remove date fixup in ../message/part.go. ParsedBuf []byte } diff --git a/webmail/api.json b/webmail/api.json index 6a11c67..9712ee1 100644 --- a/webmail/api.json +++ b/webmail/api.json @@ -1168,6 +1168,34 @@ "string" ] }, + { + "Name": "ContentDisposition", + "Docs": "", + "Typewords": [ + "string" + ] + }, + { + "Name": "ContentMD5", + "Docs": "", + "Typewords": [ + "string" + ] + }, + { + "Name": "ContentLanguage", + "Docs": "", + "Typewords": [ + "string" + ] + }, + { + "Name": "ContentLocation", + "Docs": "", + "Typewords": [ + "string" + ] + }, { "Name": "Envelope", "Docs": "Email message headers. Not for non-message parts.", @@ -2641,7 +2669,7 @@ }, { "Name": "ParsedBuf", - "Docs": "ParsedBuf message structure. Currently saved as JSON of message.Part because bstore cannot yet store recursive types. Created when first needed, and saved in the database. todo: once replaced with non-json storage, remove date fixup in ../message/part.go.", + "Docs": "ParsedBuf message structure. Currently saved as JSON of message.Part because bstore wasn't able to store recursive types when this was implemented. Created when first needed, and saved in the database. todo: once replaced with non-json storage, remove date fixup in ../message/part.go.", "Typewords": [ "[]", "uint8" diff --git a/webmail/api.ts b/webmail/api.ts index b23eb30..2bc5d71 100644 --- a/webmail/api.ts +++ b/webmail/api.ts @@ -87,6 +87,10 @@ export interface Part { ContentID: string ContentDescription: string ContentTransferEncoding: string // In upper case. + ContentDisposition: string + ContentMD5: string + ContentLanguage: string + ContentLocation: string Envelope?: Envelope | null // Email message headers. Not for non-message parts. Parts?: Part[] | null // Parts if this is a multipart. Message?: Part | null // Only for message/rfc822 and message/global. This part may have a buffer as backing io.ReaderAt, because a message/global can have a non-identity content-transfer-encoding. This part has a nil parent. @@ -378,7 +382,7 @@ export interface Message { TrainedJunk?: boolean | null // If nil, no training done yet. Otherwise, true is trained as junk, false trained as nonjunk. MsgPrefix?: string | null // Typically holds received headers and/or header separator. Preview?: string | null // If non-nil, a preview of the message based on text and/or html parts of the message. Used in the webmail and IMAP PREVIEW extension. If non-nil, it is empty if no preview could be created, or the message has not textual content or couldn't be parsed. Previews are typically created when delivering a message, but not when importing messages, for speed. Previews are generated on first request (in the webmail, or through the IMAP fetch attribute "PREVIEW" (without "LAZY")), and stored with the message at that time. The preview is at most 256 characters (can be more bytes), with detected quoted text replaced with "[...]". Previews typically end with a newline, callers may want to strip whitespace. - ParsedBuf?: string | null // ParsedBuf message structure. Currently saved as JSON of message.Part because bstore cannot yet store recursive types. Created when first needed, and saved in the database. todo: once replaced with non-json storage, remove date fixup in ../message/part.go. + ParsedBuf?: string | null // ParsedBuf message structure. Currently saved as JSON of message.Part because bstore wasn't able to store recursive types when this was implemented. Created when first needed, and saved in the database. todo: once replaced with non-json storage, remove date fixup in ../message/part.go. } // MessageEnvelope is like message.Envelope, as used in message.Part, but including @@ -606,7 +610,7 @@ export const types: TypenameMap = { "NotFilter": {"Name":"NotFilter","Docs":"","Fields":[{"Name":"Words","Docs":"","Typewords":["[]","string"]},{"Name":"From","Docs":"","Typewords":["[]","string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Subject","Docs":"","Typewords":["[]","string"]},{"Name":"Attachments","Docs":"","Typewords":["AttachmentType"]},{"Name":"Labels","Docs":"","Typewords":["[]","string"]}]}, "Page": {"Name":"Page","Docs":"","Fields":[{"Name":"AnchorMessageID","Docs":"","Typewords":["int64"]},{"Name":"Count","Docs":"","Typewords":["int32"]},{"Name":"DestMessageID","Docs":"","Typewords":["int64"]}]}, "ParsedMessage": {"Name":"ParsedMessage","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Part","Docs":"","Typewords":["Part"]},{"Name":"Headers","Docs":"","Typewords":["{}","[]","string"]},{"Name":"ViewMode","Docs":"","Typewords":["ViewMode"]},{"Name":"Texts","Docs":"","Typewords":["[]","string"]},{"Name":"HasHTML","Docs":"","Typewords":["bool"]},{"Name":"ListReplyAddress","Docs":"","Typewords":["nullable","MessageAddress"]},{"Name":"TextPaths","Docs":"","Typewords":["[]","[]","int32"]},{"Name":"HTMLPath","Docs":"","Typewords":["[]","int32"]}]}, - "Part": {"Name":"Part","Docs":"","Fields":[{"Name":"BoundaryOffset","Docs":"","Typewords":["int64"]},{"Name":"HeaderOffset","Docs":"","Typewords":["int64"]},{"Name":"BodyOffset","Docs":"","Typewords":["int64"]},{"Name":"EndOffset","Docs":"","Typewords":["int64"]},{"Name":"RawLineCount","Docs":"","Typewords":["int64"]},{"Name":"DecodedSize","Docs":"","Typewords":["int64"]},{"Name":"MediaType","Docs":"","Typewords":["string"]},{"Name":"MediaSubType","Docs":"","Typewords":["string"]},{"Name":"ContentTypeParams","Docs":"","Typewords":["{}","string"]},{"Name":"ContentID","Docs":"","Typewords":["string"]},{"Name":"ContentDescription","Docs":"","Typewords":["string"]},{"Name":"ContentTransferEncoding","Docs":"","Typewords":["string"]},{"Name":"Envelope","Docs":"","Typewords":["nullable","Envelope"]},{"Name":"Parts","Docs":"","Typewords":["[]","Part"]},{"Name":"Message","Docs":"","Typewords":["nullable","Part"]}]}, + "Part": {"Name":"Part","Docs":"","Fields":[{"Name":"BoundaryOffset","Docs":"","Typewords":["int64"]},{"Name":"HeaderOffset","Docs":"","Typewords":["int64"]},{"Name":"BodyOffset","Docs":"","Typewords":["int64"]},{"Name":"EndOffset","Docs":"","Typewords":["int64"]},{"Name":"RawLineCount","Docs":"","Typewords":["int64"]},{"Name":"DecodedSize","Docs":"","Typewords":["int64"]},{"Name":"MediaType","Docs":"","Typewords":["string"]},{"Name":"MediaSubType","Docs":"","Typewords":["string"]},{"Name":"ContentTypeParams","Docs":"","Typewords":["{}","string"]},{"Name":"ContentID","Docs":"","Typewords":["string"]},{"Name":"ContentDescription","Docs":"","Typewords":["string"]},{"Name":"ContentTransferEncoding","Docs":"","Typewords":["string"]},{"Name":"ContentDisposition","Docs":"","Typewords":["string"]},{"Name":"ContentMD5","Docs":"","Typewords":["string"]},{"Name":"ContentLanguage","Docs":"","Typewords":["string"]},{"Name":"ContentLocation","Docs":"","Typewords":["string"]},{"Name":"Envelope","Docs":"","Typewords":["nullable","Envelope"]},{"Name":"Parts","Docs":"","Typewords":["[]","Part"]},{"Name":"Message","Docs":"","Typewords":["nullable","Part"]}]}, "Envelope": {"Name":"Envelope","Docs":"","Fields":[{"Name":"Date","Docs":"","Typewords":["timestamp"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"From","Docs":"","Typewords":["[]","Address"]},{"Name":"Sender","Docs":"","Typewords":["[]","Address"]},{"Name":"ReplyTo","Docs":"","Typewords":["[]","Address"]},{"Name":"To","Docs":"","Typewords":["[]","Address"]},{"Name":"CC","Docs":"","Typewords":["[]","Address"]},{"Name":"BCC","Docs":"","Typewords":["[]","Address"]},{"Name":"InReplyTo","Docs":"","Typewords":["string"]},{"Name":"MessageID","Docs":"","Typewords":["string"]}]}, "Address": {"Name":"Address","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Host","Docs":"","Typewords":["string"]}]}, "MessageAddress": {"Name":"MessageAddress","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]}, diff --git a/webmail/msg.js b/webmail/msg.js index 2e9ac5f..dfe29bc 100644 --- a/webmail/msg.js +++ b/webmail/msg.js @@ -299,7 +299,7 @@ var api; "NotFilter": { "Name": "NotFilter", "Docs": "", "Fields": [{ "Name": "Words", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["AttachmentType"] }, { "Name": "Labels", "Docs": "", "Typewords": ["[]", "string"] }] }, "Page": { "Name": "Page", "Docs": "", "Fields": [{ "Name": "AnchorMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Count", "Docs": "", "Typewords": ["int32"] }, { "Name": "DestMessageID", "Docs": "", "Typewords": ["int64"] }] }, "ParsedMessage": { "Name": "ParsedMessage", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Part", "Docs": "", "Typewords": ["Part"] }, { "Name": "Headers", "Docs": "", "Typewords": ["{}", "[]", "string"] }, { "Name": "ViewMode", "Docs": "", "Typewords": ["ViewMode"] }, { "Name": "Texts", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HasHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListReplyAddress", "Docs": "", "Typewords": ["nullable", "MessageAddress"] }, { "Name": "TextPaths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }, { "Name": "HTMLPath", "Docs": "", "Typewords": ["[]", "int32"] }] }, - "Part": { "Name": "Part", "Docs": "", "Fields": [{ "Name": "BoundaryOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "HeaderOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "BodyOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "EndOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "RawLineCount", "Docs": "", "Typewords": ["int64"] }, { "Name": "DecodedSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "MediaType", "Docs": "", "Typewords": ["string"] }, { "Name": "MediaSubType", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTypeParams", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "ContentID", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentDescription", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTransferEncoding", "Docs": "", "Typewords": ["string"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["nullable", "Envelope"] }, { "Name": "Parts", "Docs": "", "Typewords": ["[]", "Part"] }, { "Name": "Message", "Docs": "", "Typewords": ["nullable", "Part"] }] }, + "Part": { "Name": "Part", "Docs": "", "Fields": [{ "Name": "BoundaryOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "HeaderOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "BodyOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "EndOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "RawLineCount", "Docs": "", "Typewords": ["int64"] }, { "Name": "DecodedSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "MediaType", "Docs": "", "Typewords": ["string"] }, { "Name": "MediaSubType", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTypeParams", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "ContentID", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentDescription", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTransferEncoding", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentDisposition", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentMD5", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentLanguage", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentLocation", "Docs": "", "Typewords": ["string"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["nullable", "Envelope"] }, { "Name": "Parts", "Docs": "", "Typewords": ["[]", "Part"] }, { "Name": "Message", "Docs": "", "Typewords": ["nullable", "Part"] }] }, "Envelope": { "Name": "Envelope", "Docs": "", "Fields": [{ "Name": "Date", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "Sender", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "CC", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "BCC", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "InReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }] }, "Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] }, "MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] }, diff --git a/webmail/text.js b/webmail/text.js index 1f4ce5e..e02bda2 100644 --- a/webmail/text.js +++ b/webmail/text.js @@ -299,7 +299,7 @@ var api; "NotFilter": { "Name": "NotFilter", "Docs": "", "Fields": [{ "Name": "Words", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["AttachmentType"] }, { "Name": "Labels", "Docs": "", "Typewords": ["[]", "string"] }] }, "Page": { "Name": "Page", "Docs": "", "Fields": [{ "Name": "AnchorMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Count", "Docs": "", "Typewords": ["int32"] }, { "Name": "DestMessageID", "Docs": "", "Typewords": ["int64"] }] }, "ParsedMessage": { "Name": "ParsedMessage", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Part", "Docs": "", "Typewords": ["Part"] }, { "Name": "Headers", "Docs": "", "Typewords": ["{}", "[]", "string"] }, { "Name": "ViewMode", "Docs": "", "Typewords": ["ViewMode"] }, { "Name": "Texts", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HasHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListReplyAddress", "Docs": "", "Typewords": ["nullable", "MessageAddress"] }, { "Name": "TextPaths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }, { "Name": "HTMLPath", "Docs": "", "Typewords": ["[]", "int32"] }] }, - "Part": { "Name": "Part", "Docs": "", "Fields": [{ "Name": "BoundaryOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "HeaderOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "BodyOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "EndOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "RawLineCount", "Docs": "", "Typewords": ["int64"] }, { "Name": "DecodedSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "MediaType", "Docs": "", "Typewords": ["string"] }, { "Name": "MediaSubType", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTypeParams", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "ContentID", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentDescription", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTransferEncoding", "Docs": "", "Typewords": ["string"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["nullable", "Envelope"] }, { "Name": "Parts", "Docs": "", "Typewords": ["[]", "Part"] }, { "Name": "Message", "Docs": "", "Typewords": ["nullable", "Part"] }] }, + "Part": { "Name": "Part", "Docs": "", "Fields": [{ "Name": "BoundaryOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "HeaderOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "BodyOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "EndOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "RawLineCount", "Docs": "", "Typewords": ["int64"] }, { "Name": "DecodedSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "MediaType", "Docs": "", "Typewords": ["string"] }, { "Name": "MediaSubType", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTypeParams", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "ContentID", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentDescription", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTransferEncoding", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentDisposition", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentMD5", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentLanguage", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentLocation", "Docs": "", "Typewords": ["string"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["nullable", "Envelope"] }, { "Name": "Parts", "Docs": "", "Typewords": ["[]", "Part"] }, { "Name": "Message", "Docs": "", "Typewords": ["nullable", "Part"] }] }, "Envelope": { "Name": "Envelope", "Docs": "", "Fields": [{ "Name": "Date", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "Sender", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "CC", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "BCC", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "InReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }] }, "Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] }, "MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] }, diff --git a/webmail/webmail.js b/webmail/webmail.js index 26f9dcc..0aebb28 100644 --- a/webmail/webmail.js +++ b/webmail/webmail.js @@ -299,7 +299,7 @@ var api; "NotFilter": { "Name": "NotFilter", "Docs": "", "Fields": [{ "Name": "Words", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["AttachmentType"] }, { "Name": "Labels", "Docs": "", "Typewords": ["[]", "string"] }] }, "Page": { "Name": "Page", "Docs": "", "Fields": [{ "Name": "AnchorMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Count", "Docs": "", "Typewords": ["int32"] }, { "Name": "DestMessageID", "Docs": "", "Typewords": ["int64"] }] }, "ParsedMessage": { "Name": "ParsedMessage", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Part", "Docs": "", "Typewords": ["Part"] }, { "Name": "Headers", "Docs": "", "Typewords": ["{}", "[]", "string"] }, { "Name": "ViewMode", "Docs": "", "Typewords": ["ViewMode"] }, { "Name": "Texts", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HasHTML", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListReplyAddress", "Docs": "", "Typewords": ["nullable", "MessageAddress"] }, { "Name": "TextPaths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }, { "Name": "HTMLPath", "Docs": "", "Typewords": ["[]", "int32"] }] }, - "Part": { "Name": "Part", "Docs": "", "Fields": [{ "Name": "BoundaryOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "HeaderOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "BodyOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "EndOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "RawLineCount", "Docs": "", "Typewords": ["int64"] }, { "Name": "DecodedSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "MediaType", "Docs": "", "Typewords": ["string"] }, { "Name": "MediaSubType", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTypeParams", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "ContentID", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentDescription", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTransferEncoding", "Docs": "", "Typewords": ["string"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["nullable", "Envelope"] }, { "Name": "Parts", "Docs": "", "Typewords": ["[]", "Part"] }, { "Name": "Message", "Docs": "", "Typewords": ["nullable", "Part"] }] }, + "Part": { "Name": "Part", "Docs": "", "Fields": [{ "Name": "BoundaryOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "HeaderOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "BodyOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "EndOffset", "Docs": "", "Typewords": ["int64"] }, { "Name": "RawLineCount", "Docs": "", "Typewords": ["int64"] }, { "Name": "DecodedSize", "Docs": "", "Typewords": ["int64"] }, { "Name": "MediaType", "Docs": "", "Typewords": ["string"] }, { "Name": "MediaSubType", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTypeParams", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "ContentID", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentDescription", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentTransferEncoding", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentDisposition", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentMD5", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentLanguage", "Docs": "", "Typewords": ["string"] }, { "Name": "ContentLocation", "Docs": "", "Typewords": ["string"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["nullable", "Envelope"] }, { "Name": "Parts", "Docs": "", "Typewords": ["[]", "Part"] }, { "Name": "Message", "Docs": "", "Typewords": ["nullable", "Part"] }] }, "Envelope": { "Name": "Envelope", "Docs": "", "Fields": [{ "Name": "Date", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "Sender", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "CC", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "BCC", "Docs": "", "Typewords": ["[]", "Address"] }, { "Name": "InReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }] }, "Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] }, "MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },