From 4fbf4972172e13c79948e02df1c41c2863269aa4 Mon Sep 17 00:00:00 2001 From: Robert Virkus Date: Sun, 7 Jun 2020 22:15:30 +0200 Subject: [PATCH] improve parsing of BODYSTRUCTURE, fix #64 --- lib/mime_message.dart | 30 +- lib/src/imap/fetch_parser.dart | 156 +++++--- lib/src/imap/imap_response.dart | 9 +- test/src/imap/fetch_parser_test.dart | 538 +++++++++++++++++++++++++++ 4 files changed, 673 insertions(+), 60 deletions(-) diff --git a/lib/mime_message.dart b/lib/mime_message.dart index 23c9fd09..5832fc83 100644 --- a/lib/mime_message.dart +++ b/lib/mime_message.dart @@ -611,6 +611,7 @@ class Header { /// A BODY or BODYSTRUCTURE information element class BodyPart { + /// Children parts, if present List parts; /// A string giving the content id as defined in [MIME-IMB]. @@ -637,13 +638,22 @@ class BodyPart { /// The content disposition information. This is constructed when querying BODYSTRUCTURE in a fetch. ContentDispositionHeader contentDisposition; - /// The raw text of this body part. This is set when fetching a specified part like BODY[1]. + /// The raw text of this body part. This is set when fetching a specified part like `BODY[1]`. String bodyRaw; + /// The envelope, only provided for message/rfc822 structures + Envelope envelope; + + /// The ID for fetching this body part, e.g. `1.2` for a part that can then be fetched with the criteria `BODY[1.2]`. + String get fetchId => _getFetchId(); + + BodyPart _parent; + BodyPart addPart([BodyPart childPart]) { childPart ??= BodyPart(); parts ??= []; parts.add(childPart); + childPart._parent = this; return childPart; } @@ -679,8 +689,6 @@ class BodyPart { return part.bodyRaw; } - String decodeText() {} - @override String toString() { var buffer = StringBuffer(); @@ -689,6 +697,7 @@ class BodyPart { } void write(StringBuffer buffer, [String padding = '']) { + buffer..write(padding)..write('[')..write(fetchId)..write(']\n'); if (contentType != null) { buffer.write(padding); contentType.render(buffer); @@ -715,6 +724,21 @@ class BodyPart { buffer.write(']\n'); } } + + String _getFetchId([String tail]) { + if (_parent != null) { + var index = _parent.parts.indexOf(this); + var fetchIdPart = (index + 1).toString(); + if (tail == null) { + tail = fetchIdPart; + } else { + tail = fetchIdPart + '.' + tail; + } + return _parent._getFetchId(tail); + } else { + return tail; + } + } } class Envelope { diff --git a/lib/src/imap/fetch_parser.dart b/lib/src/imap/fetch_parser.dart index 8a3666b0..65b0eaec 100644 --- a/lib/src/imap/fetch_parser.dart +++ b/lib/src/imap/fetch_parser.dart @@ -210,10 +210,28 @@ class FetchParser extends ResponseParser { message.text = textValue.value; } + /// Also compare: + /// * http://sgerwk.altervista.org/imapbodystructure.html + /// * https://tools.ietf.org/html/rfc3501#section-7.4.2 + /// * http://hea-www.cfa.harvard.edu/~fine/opinions/IMAPsucks.html void _parseBodyRecursive(BodyPart body, ImapValue bodyValue) { var isMultipartSubtypeSet = false; var multipartChildIndex = -1; var children = bodyValue.children; + if (children.length >= 7 && children[0].children == null) { + // this is a direct type: + var parsed = _parseBodyStructureFrom(children); + body.bodyRaw = parsed.bodyRaw; + body.contentDisposition = parsed.contentDisposition; + body.contentType = parsed.contentType; + body.description = parsed.description; + body.encoding = parsed.encoding; + body.envelope = parsed.envelope; + body.id = parsed.id; + body.numberOfLines = parsed.numberOfLines; + body.size = parsed.size; + return; + } for (var childIndex = 0; childIndex < children.length; childIndex++) { var child = children[childIndex]; if (child.value == null && @@ -230,45 +248,7 @@ class FetchParser extends ResponseParser { // TODO just counting cannot be a big enough indicator, compare for example ""mixed" ("charset" "utf8" "boundary" "cTOLC7EsqRfMsG")" // this is a structure value var structs = child.children; - var size = int.tryParse(structs[6].value); - var mediaType = - MediaType.fromText('${structs[0].value}/${structs[1].value}'); - var part = BodyPart() - ..id = _checkForNil(structs[3].value) - ..description = _checkForNil(structs[4].value) - ..encoding = _checkForNil(structs[5].value)?.toLowerCase() - ..size = size - ..contentType = ContentTypeHeader.from(mediaType); - var startIndex = 7; - if (mediaType.isText && - structs.length > 7 && - structs[7].value != null) { - part.numberOfLines = int.tryParse(structs[7].value); - startIndex = 8; - } - var contentTypeParameters = structs[2].children; - if (contentTypeParameters != null && contentTypeParameters.length > 1) { - for (var i = 0; i < contentTypeParameters.length; i += 2) { - var name = contentTypeParameters[i].value; - var value = contentTypeParameters[i + 1].value; - part.contentType.setParameter(name, value); - } - } - if ((structs.length > startIndex + 1) && - (structs[startIndex + 1]?.children?.isNotEmpty ?? false)) { - // exampple: [attachment, [filename, testimage.jpg, modification-date, Fri, 27 Jan 2017 16:34:4 +0100, size, 13390]] - var parts = structs[startIndex + 1].children; - var contentDisposition = - ContentDispositionHeader(parts[0].value?.toLowerCase()); - var parameters = parts[1].children; - if (parameters != null && parameters.length > 1) { - for (var i = 0; i < parameters.length; i += 2) { - contentDisposition.setParameter( - parameters[i].value, parameters[i + 1].value); - } - } - part.contentDisposition = contentDisposition; - } + var part = _parseBodyStructureFrom(structs); body.addPart(part); } else if (!isMultipartSubtypeSet) { // this is the type: @@ -288,6 +268,68 @@ class FetchParser extends ResponseParser { } } + BodyPart _parseBodyStructureFrom(List structs) { + var size = int.tryParse(structs[6].value); + var mediaType = + MediaType.fromText('${structs[0].value}/${structs[1].value}'); + var part = BodyPart() + ..id = _checkForNil(structs[3].value) + ..description = _checkForNil(structs[4].value) + ..encoding = _checkForNil(structs[5].value)?.toLowerCase() + ..size = size + ..contentType = ContentTypeHeader.from(mediaType); + var contentTypeParameters = structs[2].children; + if (contentTypeParameters != null && contentTypeParameters.length > 1) { + for (var i = 0; i < contentTypeParameters.length; i += 2) { + var name = contentTypeParameters[i].value; + var value = contentTypeParameters[i + 1].value; + part.contentType.setParameter(name, value); + } + } + var startIndex = 7; + if (mediaType.isText && structs.length > 7 && structs[7].value != null) { + part.numberOfLines = int.tryParse(structs[7].value); + startIndex = 8; + } else if (mediaType.isMessage && + mediaType.sub == MediaSubtype.messageRfc822) { + // [7] + // A body type of type MESSAGE and subtype RFC822 contains, + // immediately after the basic fields, the envelope structure, + // body structure, and size in text lines of the encapsulated + // message. + if (structs.length > 9) { + part.envelope = _parseEnvelope(null, structs[7]); + var child = BodyPart(); + part.addPart(child); + _parseBodyRecursive(child, structs[8]); + part.numberOfLines = int.tryParse(structs[9].value); + } + startIndex += 3; + } + if ((structs.length > startIndex + 1) && + (structs[startIndex + 1]?.children?.isNotEmpty ?? false)) { + // read content disposition + // example: [attachment, [filename, testimage.jpg, modification-date, Fri, 27 Jan 2017 16:34:4 +0100, size, 13390]] + var parts = structs[startIndex + 1].children; + if (parts[0].value != null) { + var contentDisposition = + ContentDispositionHeader(parts[0].value?.toLowerCase()); + var parameters = parts[1].children; + if (parameters != null && parameters.length > 1) { + for (var i = 0; i < parameters.length; i += 2) { + contentDisposition.setParameter( + parameters[i].value, parameters[i + 1].value); + } + } + part.contentDisposition = contentDisposition; + } else { + print('Unable to parse content disposition from:'); + print(parts); + } + } + return part; + } + void _parseBody(MimeMessage message, ImapValue bodyValue) { // A parenthesized list that describes the [MIME-IMB] body // structure of a message. This is computed by the server by @@ -413,7 +455,7 @@ class FetchParser extends ResponseParser { } /// parses the envelope structure of a message - void _parseEnvelope(MimeMessage message, ImapValue envelopeValue) { + Envelope _parseEnvelope(MimeMessage message, ImapValue envelopeValue) { // The fields of the envelope structure are in the following // order: [0] date, [1]subject, [2]from, [3]sender, [4]reply-to, [5]to, [6]cc, [7]bcc, // [8]in-reply-to, and [9]message-id. The date, subject, in-reply-to, @@ -426,12 +468,13 @@ class FetchParser extends ResponseParser { // of the envelope is NIL; if these header lines are present but // empty the corresponding member of the envelope is the empty // string. + Envelope envelope; var children = envelopeValue.children; //print("envelope: $children"); if (children != null && children.length >= 10) { var rawDate = _checkForNil(children[0].value); var rawSubject = _checkForNil(children[1].value); - var envelope = Envelope() + envelope = Envelope() ..date = DateCodec.decodeDate(rawDate) ..subject = MailCodec.decodeAny(rawSubject) ..from = _parseAddressList(children[2]) @@ -442,22 +485,25 @@ class FetchParser extends ResponseParser { ..bcc = _parseAddressList(children[7]) ..inReplyTo = _checkForNil(children[8].value) ..messageId = _checkForNil(children[9].value); - message.envelope = envelope; - if (rawDate != null) { - message.addHeader('Date', rawDate); - } - if (rawSubject != null) { - message.addHeader('Subject', rawSubject); + if (message != null) { + message.envelope = envelope; + if (rawDate != null) { + message.addHeader('Date', rawDate); + } + if (rawSubject != null) { + message.addHeader('Subject', rawSubject); + } + message.addHeader('In-Reply-To', envelope.inReplyTo); + message.addHeader('Message-ID', envelope.messageId); + message.from = envelope.from; + message.to = envelope.to; + message.cc = envelope.cc; + message.bcc = envelope.bcc; + message.replyTo = envelope.replyTo; + message.sender = envelope.sender; } - message.addHeader('In-Reply-To', envelope.inReplyTo); - message.addHeader('Message-ID', envelope.messageId); - message.from = envelope.from; - message.to = envelope.to; - message.cc = envelope.cc; - message.bcc = envelope.bcc; - message.replyTo = envelope.replyTo; - message.sender = envelope.sender; } + return envelope; } MailAddress _parseAddressListFirst(ImapValue addressValue) { diff --git a/lib/src/imap/imap_response.dart b/lib/src/imap/imap_response.dart index 22be4fdf..3a989f1c 100644 --- a/lib/src/imap/imap_response.dart +++ b/lib/src/imap/imap_response.dart @@ -105,8 +105,13 @@ class ImapResponse { } current = next; } else if (char == ')') { - parentheses.pop(); - current = current.parent; + var lastType = parentheses.pop(); + if (current.parent != null) { + current = current.parent; + } else { + print( + 'Warning: no parent for closing parentheses, last parentheses type $lastType'); + } } else if (char != ' ') { isInValue = true; separatorChar = ' '; diff --git a/test/src/imap/fetch_parser_test.dart b/test/src/imap/fetch_parser_test.dart index 2becaf5d..aef0dc60 100644 --- a/test/src/imap/fetch_parser_test.dart +++ b/test/src/imap/fetch_parser_test.dart @@ -228,6 +228,544 @@ void main() { 'gdpr infomedica informativa clienti.pdf'); }); + test('BODYSTRUCTURE 3', () { + var responseTexts = [ + '* 2175 FETCH (UID 3641 FLAGS (\\Seen) BODYSTRUCTURE (' + '(' + '(' + '("TEXT" "PLAIN" ("CHARSET" "UTF-8") NIL NIL "QUOTED-PRINTABLE" 274 6 NIL NIL NIL)' + '("TEXT" "HTML" ("CHARSET" "UTF-8") NIL NIL "QUOTED-PRINTABLE" 1455 30 NIL NIL NIL) ' + '"ALTERNATIVE" ("BOUNDARY" "0000000000002f322a05a71aaf69") NIL NIL' + ')' + '("IMAGE" "PNG" ("NAME" "icon.png") "" NIL "BASE64" 1986 NIL ("ATTACHMENT" ("FILENAME" "icon.png")) NIL) ' + '"RELATED" ("BOUNDARY" "0000000000002f322205a71aaf68") NIL NIL' + ')' + '("MESSAGE" "DELIVERY-STATUS" NIL NIL NIL "7BIT" 488 NIL NIL NIL)' + '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 2539 ("Tue, 2 Jun 2020 16:25:29 +0200" "tested" (("Tallah" NIL "Rocks" "domain.com")) (("Tallah" NIL "Rocks" "domain.com")) (("Tallah" NIL "Rocks" "domain.com")) (("Rocks@domain.com" NIL "Rocks" "domain.com")("Akari Haro" NIL "akari-haro" "domain.com")) NIL NIL NIL "GDQBjfh3TAG63B@domain.com") (("TEXT" "PLAIN" ("CHARSET" "utf8") NIL NIL "7BIT" 0 0 NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "utf8") NIL NIL "8BIT" 1 1 NIL NIL NIL) "ALTERNATIVE" ("BOUNDARY" "C6WuYgfyNiVn6u" "CHARSET" "utf8") NIL NIL) 51 NIL NIL NIL) "REPORT" ("BOUNDARY" "0000000000002f1f3705a71aaf47" "REPORT-TYPE" "delivery-status") NIL NIL' + ')' + ')' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + expect(messages[0].uid, 3641); + expect(messages[0].flags, ['\\Seen']); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.top, MediaToptype.multipart); + expect(body.contentType.mediaType.sub, MediaSubtype.multipartReport); + expect(body.contentType.boundary, '0000000000002f1f3705a71aaf47'); + expect(body.contentType.parameters['report-type'], 'delivery-status'); + expect(body.parts, isNotNull); + expect(body.parts.length, 3); + expect(body.parts[0].fetchId, '1'); + expect(body.parts[0].contentType, isNotNull); + expect(body.parts[0].contentType.mediaType, isNotNull); + expect(body.parts[0].contentType.mediaType.top, MediaToptype.multipart); + expect( + body.parts[0].contentType.mediaType.sub, MediaSubtype.multipartRelated); + expect(body.parts[0].contentType.boundary, '0000000000002f322205a71aaf68'); + expect(body.parts[0].parts, isNotNull); + expect(body.parts[0].parts, isNotEmpty); + expect(body.parts[0].parts.length, 2); + expect(body.parts[0].parts[0].contentType?.mediaType?.top, + MediaToptype.multipart); + expect(body.parts[0].parts[0].contentType?.mediaType?.sub, + MediaSubtype.multipartAlternative); + expect(body.parts[0].parts[0].contentType.boundary, + '0000000000002f322a05a71aaf69'); + expect(body.parts[0].parts[0].parts.length, 2); + expect(body.parts[0].parts[0].parts[0].contentType?.mediaType?.sub, + MediaSubtype.textPlain); + expect(body.parts[0].parts[0].parts[0].contentType?.charset, 'utf-8'); + expect(body.parts[0].parts[0].parts[0].encoding, 'quoted-printable'); + expect(body.parts[0].parts[0].parts[0].size, 274); + expect(body.parts[0].parts[0].parts[1].contentType?.mediaType?.sub, + MediaSubtype.textHtml); + expect(body.parts[0].parts[0].parts[1].contentType?.charset, 'utf-8'); + expect(body.parts[0].parts[0].parts[1].encoding, 'quoted-printable'); + expect(body.parts[0].parts[0].parts[1].size, 1455); + expect(body.parts[1].contentType, isNotNull); + expect(body.parts[1].contentType.mediaType, isNotNull); + expect(body.parts[1].contentType.mediaType.top, MediaToptype.message); + expect(body.parts[1].contentType.mediaType.sub, + MediaSubtype.messageDeliveryStatus); + expect(body.parts[1].size, 488); + expect(body.parts[1].encoding, '7bit'); + expect(body.parts[2].contentType.mediaType.top, MediaToptype.message); + expect(body.parts[2].contentType.mediaType.sub, MediaSubtype.messageRfc822); + expect(body.parts[2].envelope, isNotNull); + expect(body.parts[2].envelope.subject, 'tested'); + expect(body.parts[2].envelope.date, + DateCodec.decodeDate('Tue, 2 Jun 2020 16:25:29 +0200')); + expect(body.parts[2].envelope.from?.length, 1); + expect(body.parts[2].envelope.from[0].email, 'Rocks@domain.com'); + expect(body.parts[2].envelope.to?.length, 2); + expect(body.parts[2].envelope.to[0].email, 'Rocks@domain.com'); + expect(body.parts[2].envelope.to[1].email, 'akari-haro@domain.com'); + expect(body.parts[2].envelope.to[1].personalName, 'Akari Haro'); + expect(body.parts[2].parts?.length, 1); + expect(body.parts[2].parts[0].contentType?.mediaType?.top, + MediaToptype.multipart); + expect(body.parts[2].parts[0].contentType?.mediaType?.sub, + MediaSubtype.multipartAlternative); + expect(body.parts[2].parts[0].contentType?.boundary, 'C6WuYgfyNiVn6u'); + expect(body.parts[2].parts[0].contentType?.charset, 'utf8'); + expect(body.parts[2].parts[0].parts?.length, 2); + expect(body.parts[2].parts[0].parts[0].contentType?.mediaType?.sub, + MediaSubtype.textPlain); + expect(body.parts[2].parts[0].parts[0].contentType?.charset, 'utf8'); + expect(body.parts[2].parts[0].parts[0].encoding, '7bit'); + expect(body.parts[2].parts[0].parts[0].size, 0); + expect(body.parts[2].parts[0].parts[1].contentType?.mediaType?.sub, + MediaSubtype.textHtml); + expect(body.parts[2].parts[0].parts[1].contentType?.charset, 'utf8'); + expect(body.parts[2].parts[0].parts[1].encoding, '8bit'); + expect(body.parts[2].parts[0].parts[1].size, 1); + }); + + test('BODYSTRUCTURE 4 - single part', () { + var responseTexts = [ + '* 2175 FETCH (BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "iso-8859-1") NIL NIL "QUOTED-PRINTABLE" 1315 42 NIL NIL NIL NIL))' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.sub, MediaSubtype.textPlain); + expect(body.contentType.mediaType.top, MediaToptype.text); + expect(body.contentType.charset, 'iso-8859-1'); + expect(body.encoding, 'quoted-printable'); + expect(body.size, 1315); + expect(body.numberOfLines, 42); + }); + + // source: http://sgerwk.altervista.org/imapbodystructure.html + test('BODYSTRUCTURE 5 - simple alternative', () { + var responseTexts = [ + '* 1 FETCH (BODYSTRUCTURE (("TEXT" "PLAIN" ("CHARSET" "iso-8859-1") NIL NIL "QUOTED-PRINTABLE" 2234 63 NIL NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "iso-8859-1") NIL NIL "QUOTED-PRINTABLE" 2987 52 NIL NIL NIL NIL) "ALTERNATIVE" ("BOUNDARY" "d3438gr7324") NIL NIL NIL))' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.top, MediaToptype.multipart); + expect(body.contentType.mediaType.sub, MediaSubtype.multipartAlternative); + expect(body.contentType.boundary, 'd3438gr7324'); + expect(body.parts?.length, 2); + expect(body.parts[0].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[0].contentType?.mediaType?.sub, MediaSubtype.textPlain); + expect(body.parts[0].contentType?.charset, 'iso-8859-1'); + expect(body.parts[0].encoding, 'quoted-printable'); + expect(body.parts[0].size, 2234); + expect(body.parts[1].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[1].contentType?.mediaType?.sub, MediaSubtype.textHtml); + expect(body.parts[1].contentType?.charset, 'iso-8859-1'); + expect(body.parts[1].encoding, 'quoted-printable'); + expect(body.parts[1].size, 2987); + }); + + // source: http://sgerwk.altervista.org/imapbodystructure.html + test('BODYSTRUCTURE 6 - simple alternative with image', () { + var responseTexts = [ + '* 335 FETCH (BODYSTRUCTURE (("TEXT" "HTML" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 119 2 NIL ("INLINE" NIL) NIL)("IMAGE" "JPEG" ("NAME" "4356415.jpg") "<0__=rhksjt>" NIL "BASE64" 143804 NIL ("INLINE" ("FILENAME" "4356415.jpg")) NIL) "RELATED" ("BOUNDARY" "0__=5tgd3d") ("INLINE" NIL) NIL))' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.top, MediaToptype.multipart); + expect(body.contentType.mediaType.sub, MediaSubtype.multipartRelated); + expect(body.contentType.boundary, '0__=5tgd3d'); + //TODO + //expect(body.contentDisposition?.disposition, ContentDisposition.inline); + expect(body.parts?.length, 2); + expect(body.parts[0].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[0].contentType?.mediaType?.sub, MediaSubtype.textHtml); + expect(body.parts[0].contentType?.charset, 'us-ascii'); + expect(body.parts[0].encoding, '7bit'); + expect(body.parts[0].size, 119); + expect(body.parts[0].contentDisposition?.disposition, + ContentDisposition.inline); + expect(body.parts[1].contentType?.mediaType?.top, MediaToptype.image); + expect(body.parts[1].contentType?.mediaType?.sub, MediaSubtype.imageJpeg); + expect(body.parts[1].contentType?.parameters['name'], '4356415.jpg'); + expect(body.parts[1].encoding, 'base64'); + expect(body.parts[1].id, '<0__=rhksjt>'); + expect(body.parts[1].size, 143804); + expect(body.parts[1].contentDisposition?.disposition, + ContentDisposition.inline); + expect(body.parts[1].contentDisposition?.filename, '4356415.jpg'); + }); + + // source: http://sgerwk.altervista.org/imapbodystructure.html + test('BODYSTRUCTURE 7 - text + html with images', () { + var responseTexts = [ + '* 202 FETCH (BODYSTRUCTURE (("TEXT" "PLAIN" ("CHARSET" "ISO-8859-1" "FORMAT" "flowed") NIL NIL "QUOTED-PRINTABLE" 2815 73 NIL NIL NIL NIL)(("TEXT" "HTML" ("CHARSET" "ISO-8859-1") NIL NIL "QUOTED-PRINTABLE" 4171 66 NIL NIL NIL NIL)("IMAGE" "JPEG" ("NAME" "image.jpg") "<3245dsf7435>" NIL "BASE64" 189906 NIL NIL NIL NIL)("IMAGE" "GIF" ("NAME" "other.gif") "<32f6324f>" NIL "BASE64" 1090 NIL NIL NIL NIL) "RELATED" ("BOUNDARY" "--=sdgqgt") NIL NIL NIL) "ALTERNATIVE" ("BOUNDARY" "--=u5sfrj") NIL NIL NIL))' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.top, MediaToptype.multipart); + expect(body.contentType.mediaType.sub, MediaSubtype.multipartAlternative); + expect(body.contentType.boundary, '--=u5sfrj'); + expect(body.parts?.length, 2); + expect(body.parts[0].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[0].contentType?.mediaType?.sub, MediaSubtype.textPlain); + expect(body.parts[0].contentType?.charset, 'iso-8859-1'); + expect(body.parts[0].contentType?.isFlowedFormat, true); + expect(body.parts[0].encoding, 'quoted-printable'); + expect(body.parts[0].size, 2815); + // expect(body.parts[0].contentDisposition?.disposition, + // ContentDisposition.inline); + expect(body.parts[1].contentType?.mediaType?.top, MediaToptype.multipart); + expect(body.parts[1].contentType?.mediaType?.sub, + MediaSubtype.multipartRelated); + expect(body.parts[1].contentType?.boundary, '--=sdgqgt'); + expect(body.parts[1].parts?.length, 3); + expect( + body.parts[1].parts[0].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[1].parts[0].contentType?.mediaType?.sub, + MediaSubtype.textHtml); + expect(body.parts[1].parts[0].contentType?.charset, 'iso-8859-1'); + expect(body.parts[1].parts[0].encoding, 'quoted-printable'); + expect( + body.parts[1].parts[1].contentType?.mediaType?.top, MediaToptype.image); + expect(body.parts[1].parts[1].contentType?.mediaType?.sub, + MediaSubtype.imageJpeg); + expect(body.parts[1].parts[1].contentType?.parameters['name'], 'image.jpg'); + expect(body.parts[1].parts[1].id, '<3245dsf7435>'); + expect(body.parts[1].parts[1].encoding, 'base64'); + expect(body.parts[1].parts[1].size, 189906); + expect( + body.parts[1].parts[2].contentType?.mediaType?.top, MediaToptype.image); + expect(body.parts[1].parts[2].contentType?.mediaType?.sub, + MediaSubtype.imageGif); + expect(body.parts[1].parts[2].contentType?.parameters['name'], 'other.gif'); + expect(body.parts[1].parts[2].id, '<32f6324f>'); + expect(body.parts[1].parts[2].encoding, 'base64'); + expect(body.parts[1].parts[2].size, 1090); + }); + + // source: http://sgerwk.altervista.org/imapbodystructure.html + test('BODYSTRUCTURE 8 - text + html with images 2', () { + var responseTexts = [ + '* 41 FETCH (BODYSTRUCTURE ((("TEXT" "PLAIN" ("CHARSET" "ISO-8859-1") NIL NIL "QUOTED-PRINTABLE" 471 28 NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "ISO-8859-1") NIL NIL "QUOTED-PRINTABLE" 1417 36 NIL ("INLINE" NIL) NIL) "ALTERNATIVE" ("BOUNDARY" "1__=hqjksdm") NIL NIL)("IMAGE" "GIF" ("NAME" "image.gif") "<1__=cxdf2f>" NIL "BASE64" 50294 NIL ("INLINE" ("FILENAME" "image.gif")) NIL) "RELATED" ("BOUNDARY" "0__=hqjksdm") NIL NIL))' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.top, MediaToptype.multipart); + expect(body.contentType.mediaType.sub, MediaSubtype.multipartRelated); + expect(body.contentType.boundary, '0__=hqjksdm'); + expect(body.parts?.length, 2); + expect(body.parts[0].contentType?.mediaType?.top, MediaToptype.multipart); + expect(body.parts[0].contentType?.mediaType?.sub, + MediaSubtype.multipartAlternative); + expect(body.parts[0].contentType?.boundary, '1__=hqjksdm'); + expect(body.parts[0].parts?.length, 2); + expect( + body.parts[0].parts[0].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[0].parts[0].contentType?.mediaType?.sub, + MediaSubtype.textPlain); + expect(body.parts[0].parts[0].contentType?.charset, 'iso-8859-1'); + expect(body.parts[0].parts[0].encoding, 'quoted-printable'); + expect(body.parts[0].parts[0].size, 471); + expect( + body.parts[0].parts[1].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[0].parts[1].contentType?.mediaType?.sub, + MediaSubtype.textHtml); + expect(body.parts[0].parts[1].contentType?.charset, 'iso-8859-1'); + expect(body.parts[0].parts[1].encoding, 'quoted-printable'); + expect(body.parts[0].parts[1].size, 1417); + expect(body.parts[0].parts[1].contentDisposition?.disposition, + ContentDisposition.inline); + expect(body.parts[1].contentType?.mediaType?.top, MediaToptype.image); + expect(body.parts[1].contentType?.mediaType?.sub, MediaSubtype.imageGif); + expect(body.parts[1].contentType?.parameters['name'], 'image.gif'); + expect(body.parts[1].id, '<1__=cxdf2f>'); + expect(body.parts[1].encoding, 'base64'); + expect(body.parts[1].size, 50294); + expect(body.parts[1].contentDisposition?.disposition, + ContentDisposition.inline); + expect(body.parts[1].contentDisposition?.filename, 'image.gif'); + }); + + // source: http://sgerwk.altervista.org/imapbodystructure.html + test('BODYSTRUCTURE 9 - mail with attachment', () { + var responseTexts = [ + '* 302 FETCH (BODYSTRUCTURE (("TEXT" "HTML" ("CHARSET" "ISO-8859-1") NIL NIL "QUOTED-PRINTABLE" 4692 69 NIL NIL NIL NIL)("APPLICATION" "PDF" ("NAME" "pages.pdf") NIL NIL "BASE64" 38838 NIL ("attachment" ("FILENAME" "pages.pdf")) NIL NIL) "MIXED" ("BOUNDARY" "----=6fgshr") NIL NIL NIL))' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.top, MediaToptype.multipart); + expect(body.contentType.mediaType.sub, MediaSubtype.multipartMixed); + expect(body.contentType.boundary, '----=6fgshr'); + expect(body.parts?.length, 2); + expect(body.parts[0].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[0].contentType?.mediaType?.sub, MediaSubtype.textHtml); + expect(body.parts[0].contentType?.charset, 'iso-8859-1'); + expect(body.parts[0].encoding, 'quoted-printable'); + expect(body.parts[0].size, 4692); + expect(body.parts[1].contentType?.mediaType?.top, MediaToptype.application); + expect( + body.parts[1].contentType?.mediaType?.sub, MediaSubtype.applicationPdf); + expect(body.parts[1].contentType?.parameters['name'], 'pages.pdf'); + expect(body.parts[1].encoding, 'base64'); + expect(body.parts[1].size, 38838); + expect(body.parts[1].contentDisposition?.disposition, + ContentDisposition.attachment); + expect(body.parts[1].contentDisposition?.filename, 'pages.pdf'); + }); + + // source: http://sgerwk.altervista.org/imapbodystructure.html + test('BODYSTRUCTURE 10 - alternative and attachment', () { + var responseTexts = [ + '* 356 FETCH (BODYSTRUCTURE ((("TEXT" "PLAIN" ("CHARSET" "UTF-8") NIL NIL "QUOTED-PRINTABLE" 403 6 NIL NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "UTF-8") NIL NIL "QUOTED-PRINTABLE" 421 6 NIL NIL NIL NIL) "ALTERNATIVE" ("BOUNDARY" "----=fghgf3") NIL NIL NIL)("APPLICATION" "vnd.openxmlformats-officedocument.wordprocessingml.document" ("NAME" "letter.docx") NIL NIL "BASE64" 110000 NIL ("attachment" ("FILENAME" "letter.docx" "SIZE" "80384")) NIL NIL) "MIXED" ("BOUNDARY" "----=y34fgl") NIL NIL NIL))' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.top, MediaToptype.multipart); + expect(body.contentType.mediaType.sub, MediaSubtype.multipartMixed); + expect(body.contentType.boundary, '----=y34fgl'); + expect(body.parts?.length, 2); + expect(body.parts[0].fetchId, '1'); + expect(body.parts[0].contentType.mediaType.top, MediaToptype.multipart); + expect(body.parts[0].contentType.mediaType.sub, + MediaSubtype.multipartAlternative); + expect(body.parts[0].contentType.boundary, '----=fghgf3'); + expect(body.parts[0].parts?.length, 2); + expect(body.parts[0].parts[0].fetchId, '1.1'); + expect( + body.parts[0].parts[0].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[0].parts[0].contentType?.mediaType?.sub, + MediaSubtype.textPlain); + expect(body.parts[0].parts[0].contentType?.charset, 'utf-8'); + expect(body.parts[0].parts[0].encoding, 'quoted-printable'); + expect(body.parts[0].parts[0].size, 403); + expect(body.parts[0].parts[1].fetchId, '1.2'); + expect( + body.parts[0].parts[1].contentType?.mediaType?.top, MediaToptype.text); + expect(body.parts[0].parts[1].contentType?.mediaType?.sub, + MediaSubtype.textHtml); + expect(body.parts[0].parts[1].contentType?.charset, 'utf-8'); + expect(body.parts[0].parts[1].encoding, 'quoted-printable'); + expect(body.parts[0].parts[1].size, 421); + expect(body.parts[1].contentType?.mediaType?.top, MediaToptype.application); + expect(body.parts[1].fetchId, '2'); + expect(body.parts[1].contentType?.mediaType?.sub, + MediaSubtype.applicationOfficeDocumentWordProcessingDocument); + expect(body.parts[1].contentType?.parameters['name'], 'letter.docx'); + expect(body.parts[1].encoding, 'base64'); + expect(body.parts[1].size, 110000); + expect(body.parts[1].contentDisposition?.disposition, + ContentDisposition.attachment); + expect(body.parts[1].contentDisposition?.filename, 'letter.docx'); + expect(body.parts[1].contentDisposition?.size, 80384); + }); + + // source: http://sgerwk.altervista.org/imapbodystructure.html + test('BODYSTRUCTURE 11 - all together', () { + var responseTexts = [ + '* 1569 FETCH (BODYSTRUCTURE (((("TEXT" "PLAIN" ("CHARSET" "ISO-8859-1") NIL NIL "QUOTED-PRINTABLE" 833 30 NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "ISO-8859-1") NIL NIL "QUOTED-PRINTABLE" 3412 62 NIL ("INLINE" NIL) NIL) "ALTERNATIVE" ("BOUNDARY" "2__=fgrths") NIL NIL)("IMAGE" "GIF" ("NAME" "485039.gif") "<2__=lgkfjr>" NIL "BASE64" 64 NIL ("INLINE" ("FILENAME" "485039.gif")) NIL) "RELATED" ("BOUNDARY" "1__=fgrths") NIL NIL)("APPLICATION" "PDF" ("NAME" "title.pdf") "<1__=lgkfjr>" NIL "BASE64" 333980 NIL ("ATTACHMENT" ("FILENAME" "title.pdf")) NIL) "MIXED" ("BOUNDARY" "0__=fgrths") NIL NIL))' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.top, MediaToptype.multipart); + expect(body.contentType.mediaType.sub, MediaSubtype.multipartMixed); + expect(body.contentType.boundary, '0__=fgrths'); + expect(body.parts?.length, 2); + expect(body.parts[0].fetchId, '1'); + expect(body.parts[0].contentType.mediaType.top, MediaToptype.multipart); + expect( + body.parts[0].contentType.mediaType.sub, MediaSubtype.multipartRelated); + expect(body.parts[0].contentType.boundary, '1__=fgrths'); + expect(body.parts[0].parts?.length, 2); + expect(body.parts[0].parts[0].contentType.mediaType.top, + MediaToptype.multipart); + expect(body.parts[0].parts[0].contentType.mediaType.sub, + MediaSubtype.multipartAlternative); + expect(body.parts[0].parts[0].contentType.boundary, '2__=fgrths'); + expect(body.parts[0].parts[0].fetchId, '1.1'); + expect(body.parts[0].parts[0].parts?.length, 2); + expect(body.parts[0].parts[0].parts[0].fetchId, '1.1.1'); + expect(body.parts[0].parts[0].parts[0].contentType?.mediaType?.top, + MediaToptype.text); + expect(body.parts[0].parts[0].parts[0].contentType?.mediaType?.sub, + MediaSubtype.textPlain); + expect(body.parts[0].parts[0].parts[0].contentType?.charset, 'iso-8859-1'); + expect(body.parts[0].parts[0].parts[0].encoding, 'quoted-printable'); + expect(body.parts[0].parts[0].parts[0].size, 833); + expect(body.parts[0].parts[0].parts[1].fetchId, '1.1.2'); + expect(body.parts[0].parts[0].parts[1].contentType?.mediaType?.top, + MediaToptype.text); + expect(body.parts[0].parts[0].parts[1].contentType?.mediaType?.sub, + MediaSubtype.textHtml); + expect(body.parts[0].parts[0].parts[1].contentType?.charset, 'iso-8859-1'); + expect(body.parts[0].parts[0].parts[1].encoding, 'quoted-printable'); + expect(body.parts[0].parts[0].parts[1].size, 3412); + expect(body.parts[0].parts[0].parts[1].contentDisposition?.disposition, + ContentDisposition.inline); + expect(body.parts[1].fetchId, '2'); + expect(body.parts[1].contentType?.mediaType?.top, MediaToptype.application); + expect( + body.parts[1].contentType?.mediaType?.sub, MediaSubtype.applicationPdf); + expect(body.parts[1].contentType?.parameters['name'], 'title.pdf'); + expect(body.parts[1].encoding, 'base64'); + expect(body.parts[1].id, '<1__=lgkfjr>'); + expect(body.parts[1].size, 333980); + expect(body.parts[1].contentDisposition?.disposition, + ContentDisposition.attachment); + expect(body.parts[1].contentDisposition?.filename, 'title.pdf'); + }); + + // source: http://sgerwk.altervista.org/imapbodystructure.html + test('BODYSTRUCTURE 12 - single-element lists', () { + var responseTexts = [ + '* 2246 FETCH (BODYSTRUCTURE (("TEXT" "HTML" NIL NIL NIL "7BIT" 151 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----=rfsewr") NIL NIL))' + ]; + var details = ImapResponse(); + for (var text in responseTexts) { + details.add(ImapResponseLine(text)); + } + var parser = FetchParser(); + var response = Response()..status = ResponseStatus.OK; + var processed = parser.parseUntagged(details, response); + expect(processed, true); + var messages = parser.parse(details, response).messages; + expect(messages, isNotNull); + expect(messages.length, 1); + var body = messages[0].body; + //print('parsed body part: \n$body'); + expect(body, isNotNull); + expect(body.contentType, isNotNull); + expect(body.contentType.mediaType, isNotNull); + expect(body.contentType.mediaType.top, MediaToptype.multipart); + expect(body.contentType.mediaType.sub, MediaSubtype.multipartMixed); + expect(body.contentType.boundary, '----=rfsewr'); + expect(body.parts?.length, 1); + expect(body.parts[0].contentType.mediaType.top, MediaToptype.text); + expect(body.parts[0].contentType.mediaType.sub, MediaSubtype.textHtml); + expect(body.parts[0].encoding, '7bit'); + expect(body.parts[0].size, 151); + expect(body.parts[0].fetchId, '1'); + }); + test('MODSEQ', () { var responseText = '* 50 FETCH (MODSEQ (12111230047))'; var details = ImapResponse()..add(ImapResponseLine(responseText));