Skip to content

Commit

Permalink
parse BODYSTRUCTURE fetch responses, close #10
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert Virkus committed May 7, 2020
1 parent 8e57dcd commit c1c66cf
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 45 deletions.
1 change: 1 addition & 0 deletions lib/media_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ class MediaType {
/// Creates a media type from the specified text
/// The [text] must use the top/sub structure, e.g. 'text/plain'
static MediaType fromText(String text) {
text = text.toLowerCase();
var splitPos = text.indexOf('/');
if (splitPos != -1) {
var topText = text.substring(0, splitPos);
Expand Down
68 changes: 62 additions & 6 deletions lib/mime_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -617,13 +617,16 @@ class BodyAttribute {
class BodyStructure {
/// A string giving the content media type name as defined in [MIME-IMB].
/// Examples: text, image
@deprecated
String type;

/// A string giving the content subtype name as defined in [MIME-IMB].
/// Example: plain, html, png
@deprecated
String subtype;

/// body parameter parenthesized list as defined in [MIME-IMB].
@deprecated
List<BodyAttribute> attributes = <BodyAttribute>[];

/// A string giving the content id as defined in [MIME-IMB].
Expand All @@ -633,7 +636,7 @@ class BodyStructure {
String description;

/// A string giving the content transfer encoding as defined in [MIME-IMB].
/// Examples: 7bit, utf-8, US-ASCII
/// Examples: base64, quoted-printable
String encoding;

/// A number giving the size of the body in octets.
Expand All @@ -644,9 +647,19 @@ class BodyStructure {
/// Some message types like MESSAGE/RFC822 or TEXT also provide the number of lines
int numberOfLines;

/// The content type infomation.
ContentTypeHeader contentType;

/// The content disposition information. This is constructed when querying BODYSTRUCTURE in a fetch.
ContentDispositionHeader contentDisposition;

BodyStructure(this.type, this.subtype, this.id, this.description,
this.encoding, this.size);
this.encoding, this.size) {
var mediaType = MediaType.fromText('$type/$subtype');
contentType = ContentTypeHeader.from(mediaType);
}

@deprecated
void addAttribute(String name, String value) {
attributes.add(BodyAttribute(name, value));
}
Expand All @@ -655,7 +668,9 @@ class BodyStructure {
class Body {
List<BodyStructure> structures = <BodyStructure>[];
List<String> parts = <String>[];
@deprecated
String type;
ContentTypeHeader contentType;

void addStructure(BodyStructure structure) {
structures.add(structure);
Expand Down Expand Up @@ -697,15 +712,19 @@ class ParameterizedHeader {
} else {
var name = element.substring(0, splitPos).toLowerCase();
var value = element.substring(splitPos + 1);
var valueWithoutQuotes = value;
if (value.startsWith('"') && value.endsWith('"')) {
valueWithoutQuotes = value.substring(1, value.length - 1);
}
var valueWithoutQuotes = removeQuotes(value);
parameters[name] = valueWithoutQuotes;
}
}
}

String removeQuotes(String value) {
if (value.startsWith('"') && value.endsWith('"')) {
return value.substring(1, value.length - 1);
}
return value;
}

void renderField(String name, String value, bool quote, StringBuffer buffer) {
if (value == null) {
return;
Expand Down Expand Up @@ -780,6 +799,22 @@ class ContentTypeHeader extends ParameterizedHeader {
return buffer.toString();
}

@override
void setParameter(String name, String quotedValue) {
name = name.toLowerCase();
if (name == 'charset') {
quotedValue = removeQuotes(quotedValue).toLowerCase();
charset = quotedValue;
} else if (name == 'boundary') {
quotedValue = removeQuotes(quotedValue);
boundary = quotedValue;
} else if (name == 'format') {
quotedValue = removeQuotes(quotedValue).toLowerCase();
isFlowedFormat = (quotedValue == 'flowed');
}
super.setParameter(name, quotedValue);
}

static ContentTypeHeader from(MediaType mediaType,
{String charset, String boundary, bool isFlowedFormat}) {
var type = ContentTypeHeader(mediaType.text);
Expand Down Expand Up @@ -883,4 +918,25 @@ class ContentDispositionHeader extends ParameterizedHeader {
]);
return buffer.toString();
}

@override
void setParameter(String name, String quotedValue) {
name = name.toLowerCase();
if (name == 'filename') {
quotedValue = removeQuotes(quotedValue).toLowerCase();
filename = quotedValue;
} else if (name == 'creation-date') {
quotedValue = removeQuotes(quotedValue);
creationDate = DateCodec.decodeDate(quotedValue);
} else if (name == 'modification-date') {
quotedValue = removeQuotes(quotedValue);
modificationDate = DateCodec.decodeDate(quotedValue);
} else if (name == 'read-date') {
quotedValue = removeQuotes(quotedValue);
readDate = DateCodec.decodeDate(quotedValue);
} else if (name == 'size') {
size = int.tryParse(quotedValue);
}
super.setParameter(name, quotedValue);
}
}
115 changes: 104 additions & 11 deletions lib/src/imap/fetch_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ class FetchParser extends ResponseParser<List<MimeMessage>> {
case 'BODY':
_parseBody(message, child);
break;
case 'BODYSTRUCTURE':
_parseBodyStructure(message, child);
break;
case 'BODY[HEADER]':
case 'RFC822.HEADER':
if (hasNext) {
Expand Down Expand Up @@ -232,12 +235,68 @@ class FetchParser extends ResponseParser<List<MimeMessage>> {
// fields, the size of the body in text lines. Note that this
// size is the size in its content transfer encoding and not the
// resulting size after any decoding.

// Extension data follows the multipart subtype. Extension data
// is never returned with the BODY fetch, but can be returned with
// a BODYSTRUCTURE fetch. Extension data, if present, MUST be in
// the defined order. The extension data of a multipart body part
// are in the following order:

// [7 / 8]
// body parameter parenthesized list
// A parenthesized list of attribute/value pairs [e.g., ("foo"
// "bar" "baz" "rag") where "bar" is the value of "foo", and
// "rag" is the value of "baz"] as defined in [MIME-IMB].

// [8 / 9]
// body disposition
// A parenthesized list, consisting of a disposition type
// string, followed by a parenthesized list of disposition
// attribute/value pairs as defined in [DISPOSITION].

// [9 / 10]
// body language
// A string or parenthesized list giving the body language
// value as defined in [LANGUAGE-TAGS].

// [10 / 11]
// body location
// A string list giving the body content URI as defined in
// [LOCATION].
//
//
// The extension data of a non-multipart body part are in the
// following order:

// [7 / 8]
// body MD5
// A string giving the body MD5 value as defined in [MD5].
//
// [8 / 9]
// body disposition
// A parenthesized list with the same content and function as
// the body disposition for a multipart body part.

// [9 / 10]
// body language
// A string or parenthesized list giving the body language
// value as defined in [LANGUAGE-TAGS].

// [10 / 11]
// body location
// A string list giving the body content URI as defined in
// [LOCATION].
var children = bodyValue.children;
//print('body: $bodyValue');
var body = Body();
var isBodyTypeSet = false;
for (var child in children) {
if (child.children != null && child.children.length >= 7) {
var isMultipartSubtypeSet = false;
var multipartChildIndex = -1;
for (var childIndex = 0; childIndex < children.length; childIndex++) {
var child = children[childIndex];
if (!isMultipartSubtypeSet &&
child.children != null &&
child.children.length >= 7) {
// 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);
Expand All @@ -248,26 +307,60 @@ class FetchParser extends ResponseParser<List<MimeMessage>> {
_checkForNil(structs[4].value),
structs[5].value,
size);
if (structs.length > 7 && structs[7].value != null) {
var startIndex = 7;
if (structure.contentType.mediaType.isText &&
structs.length > 7 &&
structs[7].value != null) {
structure.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;
structure.addAttribute(name, value);
structure.contentType.setParameter(name, value);
}
}
var attributeValues = structs[2].children;
if (attributeValues != null && attributeValues.length > 1) {
for (var i = 0; i < attributeValues.length; i += 2) {
structure.addAttribute(
attributeValues[i].value, attributeValues[i + 1].value);
if ((structs.length > startIndex + 1) &&
(structs[startIndex + 1]?.children?.isNotEmpty ?? false)) {
// exampple: <null>[attachment, <null>[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);
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);
}
}
structure.contentDisposition = contentDisposition;
}
body.addStructure(structure);
} else if (!isBodyTypeSet) {
} else if (!isMultipartSubtypeSet) {
// this is the type:
isBodyTypeSet = true;
isMultipartSubtypeSet = true;
multipartChildIndex = childIndex;
body.type = child.value;
body.contentType = ContentTypeHeader('multipart/${child.value}');
} else if (childIndex == multipartChildIndex + 1 &&
child.children != null &&
child.children.length > 1) {
var parameters = child.children;
for (var i = 0; i < parameters.length; i += 2) {
body.contentType
.setParameter(parameters[i].value, parameters[i + 1].value);
}
}
message.body = body;
}
}

void _parseBodyStructure(MimeMessage message, ImapValue bodyValue) {
_parseBody(message, bodyValue);
}

/// parses the envelope structure of a message
void _parseEnvelope(MimeMessage message, ImapValue envelopeValue) {
// The fields of the envelope structure are in the following
Expand Down
48 changes: 20 additions & 28 deletions test/imap/imap_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -414,31 +414,28 @@ void main() {
expect(message.to.first.mailboxName, 'alice.dev');
expect(message.to.first.hostName, 'domain.com');
expect(message.body, isNotNull);
expect(message.body.type, 'alternative');
expect(message.body.contentType, isNotNull);
expect(message.body.contentType.mediaType.sub,
MediaSubtype.multipartAlternative);
expect(message.body.structures, isNotNull);
expect(message.body.structures.length, 2);
expect(message.body.structures[0].type, 'text');
expect(message.body.structures[0].subtype, 'plain');
expect(message.body.structures[0].contentType, isNotNull);
expect(message.body.structures[0].contentType.mediaType.sub,
MediaSubtype.textPlain);
expect(message.body.structures[0].description, null);
expect(message.body.structures[0].id, null);
expect(message.body.structures[0].encoding, 'quoted-printable');
expect(message.body.structures[0].size, 1289);
expect(message.body.structures[0].numberOfLines, 53);
expect(message.body.structures[0].attributes, isNotNull);
expect(message.body.structures[0].attributes.length, 1);
expect(message.body.structures[0].attributes[0].name, 'charset');
expect(message.body.structures[0].attributes[0].value, 'UTF-8');
expect(message.body.structures[1].type, 'text');
expect(message.body.structures[1].subtype, 'html');
expect(message.body.structures[0].contentType.charset, 'utf-8');
expect(message.body.structures[1].contentType.mediaType.sub,
MediaSubtype.textHtml);
expect(message.body.structures[1].description, null);
expect(message.body.structures[1].id, null);
expect(message.body.structures[1].encoding, 'quoted-printable');
expect(message.body.structures[1].size, 7496);
expect(message.body.structures[1].numberOfLines, 302);
expect(message.body.structures[1].attributes, isNotNull);
expect(message.body.structures[1].attributes.length, 1);
expect(message.body.structures[1].attributes[0].name, 'charset');
expect(message.body.structures[1].attributes[0].value, 'UTF-8');
expect(message.body.structures[1].contentType.charset, 'utf-8');

message = fetchResponse.result[1];
expect(message.sequenceId, lowerIndex);
Expand Down Expand Up @@ -480,34 +477,29 @@ void main() {
expect(message.to.first.mailboxName, 'alice.dev');
expect(message.to.first.hostName, 'domain.com');
expect(message.body, isNotNull);
expect(message.body.type, 'MIXED');
expect(
message.body.contentType.mediaType.sub, MediaSubtype.multipartMixed);
expect(message.body.structures, isNotNull);
expect(message.body.structures.length, 2);
expect(message.body.structures[0].type, 'TEXT');
expect(message.body.structures[0].subtype, 'PLAIN');
expect(message.body.structures[0].contentType.mediaType.sub,
MediaSubtype.textPlain);
expect(message.body.structures[0].description, null);
expect(message.body.structures[0].id, null);
expect(message.body.structures[0].encoding, '7BIT');
expect(message.body.structures[0].size, 1152);
expect(message.body.structures[0].numberOfLines, 23);
expect(message.body.structures[0].attributes, isNotNull);
expect(message.body.structures[0].attributes.length, 1);
expect(message.body.structures[0].attributes[0].name, 'CHARSET');
expect(message.body.structures[0].attributes[0].value, 'US-ASCII');
expect(message.body.structures[1].type, 'TEXT');
expect(message.body.structures[1].subtype, 'PLAIN');
expect(message.body.structures[0].contentType.charset, 'us-ascii');
expect(message.body.structures[1].contentType.mediaType.sub,
MediaSubtype.textPlain);
expect(message.body.structures[1].description, 'Compiler diff');
expect(message.body.structures[1].id,
'<[email protected]>');
expect(message.body.structures[1].encoding, 'BASE64');
expect(message.body.structures[1].size, 4554);
expect(message.body.structures[1].numberOfLines, 73);
expect(message.body.structures[1].attributes, isNotNull);
expect(message.body.structures[1].attributes.length, 2);
expect(message.body.structures[1].attributes[0].name, 'CHARSET');
expect(message.body.structures[1].attributes[0].value, 'US-ASCII');
expect(message.body.structures[1].attributes[1].name, 'NAME');
expect(message.body.structures[1].attributes[1].value, 'cc.diff');
expect(message.body.structures[1].contentType.charset, 'us-ascii');
expect(
message.body.structures[1].contentType.parameters['name'], 'cc.diff');
}
});

Expand Down
Loading

0 comments on commit c1c66cf

Please sign in to comment.