1 /**
2 * Parses and handles Internet mail/news messages.
3 *
4 * License:
5 * This Source Code Form is subject to the terms of
6 * the Mozilla Public License, v. 2.0. If a copy of
7 * the MPL was not distributed with this file, You
8 * can obtain one at http://mozilla.org/MPL/2.0/.
9 *
10 * Authors:
11 * Vladimir Panteleev <ae@cy.md>
12 */
13
14 module ae.net.ietf.message;
15
16 import std.algorithm;
17 import std.array;
18 import std.base64;
19 import std.conv;
20 import std.datetime;
21 import std.exception;
22 import std.regex;
23 import std.string;
24 import std.uri;
25 import std.utf;
26
27 // TODO: Replace with logging?
28 debug(RFC850) import std.stdio : stderr;
29
30 import ae.net.ietf.headers;
31 import ae.utils.array;
32 import ae.utils.iconv;
33 import ae.utils.mime;
34 import ae.utils.regex;
35 import ae.utils.text;
36 import ae.utils.time;
37
38 import ae.net.ietf.wrap;
39
40 alias std..string.indexOf indexOf;
41
42 struct Xref
43 {
44 string group;
45 int num;
46 }
47
48 class Rfc850Message
49 {
50 /// The raw message (as passed in a constructor).
51 ascii message;
52
53 /// The message ID, as specified at creation or in the Message-ID field.
54 /// Includes the usual angular brackets.
55 string id;
56
57 /// Cross-references - for newsgroups posts, list of groups where it was
58 /// posted, and article number in said group.
59 Xref[] xref;
60
61 /// The thread subject, with the leading "Re: " and list ID stripped.
62 string subject;
63
64 /// The original message subject, as it appears in the message.
65 string rawSubject;
66
67 /// The author's name, in UTF-8, stripped of quotes (no email address).
68 string author;
69
70 /// The author's email address, stripped of angular brackets.
71 string authorEmail;
72
73 /// Message date/time.
74 SysTime time;
75
76 /// A list of Message-IDs that this post is in reply to.
77 /// The most recent message (and direct parent) comes last.
78 string[] references;
79
80 /// Whether this post is a reply.
81 bool reply;
82
83 /// This message's headers.
84 Headers headers;
85
86 /// The raw body of this message (or message part),
87 /// i.e. the part of `this.message` after the headers.
88 ascii rawContent;
89
90 /// The text contents of this message (UTF-8).
91 /// "null" in case of an error.
92 string content;
93
94 /// The contents of this message (depends on mimeType).
95 ubyte[] data;
96
97 /// Explanation for null content.
98 string error;
99
100 /// Reflow options (RFC 2646).
101 bool flowed, delsp;
102
103 /// Additional Content-Type "markup" option.
104 string markup;
105
106 /// For a multipart message, contains the child parts.
107 /// May nest more than one level.
108 Rfc850Message[] parts;
109
110 /// Properties of a multipart message's part.
111 string name, fileName, description, mimeType;
112
113 /// Parses a message string and creates a Rfc850Message.
114 this(ascii message)
115 {
116 this.message = message;
117 debug(RFC850) scope(failure) stderr.writeln("Failure while parsing message: ", id);
118
119 // Split headers from message, parse headers
120
121 // TODO: this breaks binary encodings, FIXME
122 auto text = message.fastReplace("\r\n", "\n");
123 auto headerEnd = text.indexOf("\n\n");
124 if (headerEnd < 0) headerEnd = text.length;
125 auto header = text[0..headerEnd];
126 header = header.fastReplace("\n\t", " ").fastReplace("\n ", " ");
127
128 // TODO: Use a proper spec-conforming header parser
129 foreach (s; header.fastSplit('\n'))
130 {
131 if (s == "") break;
132
133 auto p = s.indexOf(": ");
134 if (p<0) continue;
135 //assert(p>0, "Bad header line: " ~ s);
136 headers[s[0..p]] = s[p+2..$];
137 }
138
139 // Decode international characters in headers
140
141 string defaultEncoding = guessDefaultEncoding(headers.get("User-Agent", null));
142
143 foreach (string key, ref string value; headers)
144 if (hasHighAsciiChars(value))
145 value = decodeEncodedText(value, defaultEncoding);
146
147 // Decode transfer encoding
148
149 rawContent = text[min(headerEnd+2, $)..$];
150
151 if ("Content-Transfer-Encoding" in headers)
152 try
153 rawContent = decodeTransferEncoding(rawContent, headers["Content-Transfer-Encoding"]);
154 catch (Exception e)
155 {
156 rawContent = null;
157 error = "Error decoding " ~ headers["Content-Transfer-Encoding"] ~ " message: " ~ e.msg;
158 }
159
160 // Decode message
161
162 data = cast(ubyte[])rawContent;
163
164 TokenHeader contentType, contentDisposition;
165 if ("Content-Type" in headers)
166 contentType = decodeTokenHeader(headers["Content-Type"]);
167 if ("Content-Disposition" in headers)
168 contentDisposition = decodeTokenHeader(headers["Content-Disposition"]);
169 mimeType = toLower(contentType.value);
170 flowed = contentType.properties.get("format", "fixed").icmp("flowed")==0;
171 delsp = contentType.properties.get("delsp", "no").icmp("yes") == 0;
172 markup = contentType.properties.get("markup", null);
173
174 if (rawContent)
175 {
176 if (!mimeType || mimeType == "text/plain")
177 {
178 if ("charset" in contentType.properties)
179 content = decodeEncodedText(rawContent, contentType.properties["charset"]);
180 else
181 content = decodeEncodedText(rawContent, defaultEncoding);
182 }
183 else
184 if (mimeType.startsWith("multipart/") && "boundary" in contentType.properties)
185 {
186 string boundary = contentType.properties["boundary"];
187 auto end = rawContent.indexOf("--" ~ boundary ~ "--");
188 if (end < 0)
189 end = rawContent.length;
190 rawContent = rawContent[0..end];
191
192 auto rawParts = rawContent.split("--" ~ boundary ~ "\n");
193 foreach (rawPart; rawParts[1..$])
194 {
195 auto part = new Rfc850Message(rawPart);
196 if (part.content && !content)
197 content = part.content;
198 parts ~= part;
199 }
200
201 if (!content)
202 {
203 if (rawParts.length && rawParts[0].asciiStrip().length)
204 content = rawParts[0]; // default content to multipart stub
205 else
206 error = "Couldn't find text part in this " ~ mimeType ~ " message";
207 }
208 }
209 else
210 error = "Don't know how parse " ~ mimeType ~ " message";
211 }
212
213 // Strip PGP signature away to a separate "attachment"
214
215 enum PGP_START = "-----BEGIN PGP SIGNED MESSAGE-----\n";
216 enum PGP_DELIM = "\n-----BEGIN PGP SIGNATURE-----\n";
217 enum PGP_END = "\n-----END PGP SIGNATURE-----";
218 if (content.startsWith(PGP_START) &&
219 content.contains(PGP_DELIM) &&
220 content.asciiStrip().endsWith(PGP_END))
221 {
222 // Don't attempt to create meaningful signature files... just get the clutter out of the way
223 content = content.asciiStrip();
224 auto p = content.indexOf(PGP_DELIM);
225 auto part = new Rfc850Message(content[p+PGP_DELIM.length..$-PGP_END.length]);
226 content = content[PGP_START.length..p];
227 p = content.indexOf("\n\n");
228 if (p >= 0)
229 content = content[p+2..$];
230 part.fileName = "pgp.sig";
231 parts ~= part;
232 }
233
234 // Decode UU-encoded attachments
235
236 if (content.contains("\nbegin "))
237 {
238 auto r = regex(`^begin [0-7]+ \S+$`);
239 auto lines = content.split("\n");
240 size_t start;
241 bool started;
242 string fn;
243
244 for (size_t i=0; i<lines.length; i++)
245 if (!started && !match(lines[i], r).empty)
246 {
247 start = i;
248 fn = lines[i].split(" ")[2];
249 started = true;
250 }
251 else
252 if (started && lines[i] == "end" && lines[i-1]=="`")
253 {
254 started = false;
255 try
256 {
257 auto data = uudecode(lines[start+1..i]);
258
259 auto part = new Rfc850Message();
260 part.fileName = fn;
261 part.mimeType = guessMime(fn);
262 part.data = data;
263 parts ~= part;
264
265 lines = lines[0..start] ~ lines[i+1..$];
266 i = start-1;
267 }
268 catch (Exception e)
269 debug(RFC850) stderr.writeln(e);
270 }
271
272 content = lines.join("\n");
273 }
274
275 // Parse message-part properties
276
277 name = contentType.properties.get("name", string.init);
278 fileName = contentDisposition.properties.get("filename", string.init);
279 description = headers.get("Content-Description", string.init);
280 if (name == fileName)
281 name = null;
282
283 // Decode references
284
285 if ("References" in headers)
286 {
287 reply = true;
288 auto refs = asciiStrip(headers["References"]);
289 while (refs.startsWith("<"))
290 {
291 auto p = refs.indexOf(">");
292 if (p < 0)
293 break;
294 references ~= refs[0..p+1];
295 refs = asciiStrip(refs[p+1..$]);
296 }
297 }
298 else
299 if ("In-Reply-To" in headers)
300 references = [headers["In-Reply-To"]];
301
302 // Decode subject
303
304 subject = rawSubject = "Subject" in headers ? decodeRfc1522(headers["Subject"]) : null;
305 if (subject.startsWith("Re: "))
306 {
307 subject = subject[4..$];
308 reply = true;
309 }
310
311 // Decode author
312
313 static string[2] decodeAuthor(string header)
314 {
315 string author, authorEmail;
316 author = authorEmail = header;
317 if ((author.indexOf('@') < 0 && author.indexOf(" at ") >= 0)
318 || (author.indexOf("<") < 0 && author.indexOf(">") < 0 && author.indexOf(" (") > 0 && author.endsWith(")")))
319 {
320 // Mailing list archive format
321 assert(author == authorEmail);
322 if (author.indexOf(" (") > 0 && author.endsWith(")"))
323 {
324 authorEmail = author[0 .. author.lastIndexOf(" (")].replace(" at ", "@");
325 author = author[author.lastIndexOf(" (")+2 .. $-1].decodeRfc1522();
326 }
327 else
328 {
329 authorEmail = author.replace(" at ", "@");
330 author = author[0 .. author.lastIndexOf(" at ")];
331 }
332 }
333 if (author.indexOf('<')>=0 && author.endsWith('>'))
334 {
335 auto p = author.indexOf('<');
336 authorEmail = author[p+1..$-1];
337 author = decodeRfc1522(asciiStrip(author[0..p]));
338 }
339
340 if (author.length>2 && author[0]=='"' && author[$-1]=='"')
341 author = decodeRfc1522(asciiStrip(author[1..$-1]));
342 if ((author == authorEmail || author == "") && authorEmail.indexOf("@") > 0)
343 author = authorEmail[0..authorEmail.indexOf("@")];
344 return [author, authorEmail];
345 }
346
347 list(author, authorEmail) = decodeAuthor("From" in headers ? decodeRfc1522(headers["From"]) : null);
348
349 if (headers.get("List-Post", null) == "<mailto:" ~ authorEmail ~ ">" && "Reply-To" in headers)
350 list(null, authorEmail) = decodeAuthor(decodeRfc1522(headers["Reply-To"].findSplit(", ")[0]));
351
352 // Decode cross-references
353
354 if ("Xref" in headers)
355 {
356 auto xrefStrings = split(headers["Xref"], " ")[1..$];
357 foreach (str; xrefStrings)
358 {
359 auto segs = str.split(":");
360 xref ~= Xref(segs[0], to!int(segs[1]));
361 }
362 }
363 else
364 if (headers.get("Sender", null).canFind("-bounces@") && !headers["Sender"].representation.any!(c => c.among('<', '>', '"')))
365 xref = [Xref(headers["Sender"].findSplit("-bounces@")[0])];
366 else
367 if ("List-Unsubscribe" in headers)
368 xref = headers["List-Unsubscribe"].split(", ").filter!(s => s.canFind("/options/")).map!(s => Xref(s.split("/")[$-1].stripRight('>'))).array();
369 else
370 if ("List-ID" in headers)
371 {
372 auto listID = headers["List-ID"];
373 auto parts = listID.findSplit(" <");
374 if (parts[1].length)
375 listID = parts[2].findSplit(".")[0];
376 else
377 listID = parts[0].replace(`"`, ``);
378 xref = [Xref(listID)];
379 }
380
381 // Decode message ID
382
383 if ("Message-ID" in headers && !id)
384 id = headers["Message-ID"];
385
386 // Decode post time
387
388 time = Clock.currTime; // default value
389
390 if (auto pdate = "X-Original-Date" in headers)
391 time = parseDate(*pdate, time);
392 else
393 if (auto pdate = "NNTP-Posting-Date" in headers)
394 time = parseDate(*pdate, time);
395 else
396 if (auto pdate = "Date" in headers)
397 time = parseDate(*pdate, time);
398 }
399
400 private this() {} // for attachments and templates
401
402 /// Create a template Rfc850Message for a new posting to the specified groups.
403 static Rfc850Message newPostTemplate(string groups)
404 {
405 auto post = new Rfc850Message();
406 foreach (group; groups.split(","))
407 post.xref ~= Xref(group);
408 return post;
409 }
410
411 private static SysTime parseDate(string str, SysTime defaultTime)
412 {
413 str = str.strip();
414 str = str.replace(re!`([+\-]\d\d\d\d) \(.*\)$`, "$1");
415 try
416 return parseTime!(TimeFormats.RFC850)(str);
417 catch (Exception e)
418 try
419 return parseTime!(`D, j M Y H:i:s O`)(str);
420 catch (Exception e)
421 try
422 return parseTime!(`D, j M Y H:i:s e`)(str);
423 catch (Exception e)
424 try
425 return parseTime!(`D, j M Y H:i O`)(str);
426 catch (Exception e)
427 try
428 return parseTime!(`D, j M Y H:i e`)(str);
429 catch (Exception e)
430 {
431 // fall-back to default (class creation time)
432 // TODO: better behavior?
433 return defaultTime;
434 }
435 }
436
437 @property WrapFormat wrapFormat()
438 {
439 return flowed ? delsp ? WrapFormat.flowedDelSp : WrapFormat.flowed : WrapFormat.heuristics;
440 }
441
442 /// Create a template Rfc850Message for a reply to this message.
443 Rfc850Message replyTemplate()
444 {
445 auto post = new Rfc850Message();
446 post.reply = true;
447 post.xref = this.xref;
448 post.references = this.references ~ this.id;
449 post.subject = this.rawSubject;
450 if (!post.subject.startsWith("Re:"))
451 post.subject = "Re: " ~ post.subject;
452
453 auto paragraphs = unwrapText(this.content, this.wrapFormat);
454 foreach (i, ref paragraph; paragraphs)
455 if (paragraph.quotePrefix.length)
456 paragraph.quotePrefix = ">" ~ paragraph.quotePrefix;
457 else
458 {
459 if (paragraph.text == "-- " || paragraph.text == "_______________________________________________")
460 {
461 paragraphs = paragraphs[0..i];
462 break;
463 }
464 paragraph.quotePrefix = paragraph.text.length ? "> " : ">";
465 }
466 while (paragraphs.length && paragraphs[$-1].text.length==0)
467 paragraphs = paragraphs[0..$-1];
468
469 auto replyTime = time;
470 replyTime.timezone = UTC();
471 post.content =
472 "On " ~ replyTime.formatTime!`l, j F Y \a\t H:i:s e`() ~ ", " ~ this.author ~ " wrote:\n" ~
473 wrapText(paragraphs) ~
474 "\n\n";
475 post.flowed = true;
476 post.delsp = false;
477
478 return post;
479 }
480
481 /// Set the message text.
482 /// Rewraps as necessary.
483 void setText(string text)
484 {
485 this.content = wrapText(unwrapText(text, WrapFormat.input));
486 this.flowed = true;
487 this.delsp = false;
488 }
489
490 /// Write this Message instance's fields to their appropriate headers.
491 void compileHeaders()
492 {
493 assert(id);
494
495 headers["Message-ID"] = id;
496 headers["From"] = format(`%s <%s>`, author, authorEmail);
497 headers["Subject"] = subject;
498 headers["Newsgroups"] = xref.map!(x => x.group)().join(",");
499 headers["Content-Type"] = format("text/plain; charset=utf-8; format=%s; delsp=%s%s",
500 flowed ? "flowed" : "fixed",
501 delsp ? "yes" : "no",
502 markup ? "; markup=" ~ markup : null,
503 );
504 headers["Content-Transfer-Encoding"] = "8bit";
505 if (references.length)
506 {
507 headers["References"] = references.join(" ");
508 headers["In-Reply-To"] = references[$-1];
509 }
510 if (time == SysTime.init)
511 time = Clock.currTime();
512 headers["Date"] = time.formatTime!(TimeFormats.RFC2822);
513 headers["User-Agent"] = "ae.net.ietf.message";
514 }
515
516 /// Construct the headers and message fields.
517 void compile()
518 {
519 compileHeaders();
520
521 string[] lines;
522 foreach (name, value; headers)
523 {
524 if (value.hasHighAsciiChars())
525 value = value.encodeRfc1522();
526 auto line = name ~ ": " ~ value;
527 auto lineStart = name.length + 2;
528
529 foreach (c; line)
530 enforce(c >= 32, "Control characters in header: %(%s%)".format([line]));
531
532 while (line.length >= 80)
533 {
534 auto p = line[0..80].lastIndexOf(' ');
535 if (p < lineStart)
536 {
537 p = 80 + line[80..$].indexOf(' ');
538 if (p < 80)
539 break;
540 }
541 lines ~= line[0..p];
542 line = line[p..$];
543 lineStart = 1;
544 }
545 lines ~= line;
546 }
547
548 message =
549 lines.join("\r\n") ~
550 "\r\n\r\n" ~
551 splitAsciiLines(content).join("\r\n");
552 }
553
554 /// Get the Message-ID that this message is in reply to.
555 @property string parentID()
556 {
557 return references.length ? references[$-1] : null;
558 }
559
560 /// Return the oldest known ancestor of this post, possibly
561 /// this post's ID if it is the first one in the thread.
562 /// May not be the thread ID - some UAs/services
563 /// cut off or strip the "References" header.
564 @property string firstAncestorID()
565 {
566 return references.length ? references[0] : id;
567 }
568 }
569
570 unittest
571 {
572 auto post = new Rfc850Message("From: msonke at example.org (=?ISO-8859-1?Q?S=F6nke_Martin?=)\n\nText");
573 assert(post.author == "Sönke Martin");
574 assert(post.authorEmail == "msonke@example.org");
575
576 post = new Rfc850Message("Date: Tue, 06 Sep 2011 14:52 -0700\n\nText");
577 assert(post.time.year == 2011);
578
579 post = new Rfc850Message("List-ID: phobos\nSubject: [phobos] Subject\n\nText");
580 assert(post.xref == [Xref("phobos")]);
581
582 post = new Rfc850Message("Sender: dmd-beta-bounces@puremagic.com\n\nText");
583 assert(post.xref == [Xref("dmd-beta")]);
584 }
585
586 private:
587
588 /// Decode headers with international characters in them.
589 string decodeRfc1522(string str)
590 {
591 auto words = str.split(" ");
592 bool[] encoded = new bool[words.length];
593
594 foreach (wordIndex, ref word; words)
595 if (word.length > 6 && word.startsWith("=?") && word.endsWith("?="))
596 {
597 auto parts = split(word[2..$-2], "?");
598 if (parts.length != 3)
599 continue;
600 auto charset = parts[0];
601 auto encoding = parts[1];
602 auto text = parts[2];
603
604 switch (toUpper(encoding))
605 {
606 case "Q":
607 text = decodeQuotedPrintable(text, true);
608 break;
609 case "B":
610 text = cast(ascii)Base64.decode(text);
611 break;
612 default:
613 continue /*foreach*/;
614 }
615
616 word = decodeEncodedText(text, charset);
617 encoded[wordIndex] = true;
618 }
619
620 string result;
621 foreach (wordIndex, word; words)
622 {
623 if (wordIndex > 0 && !(encoded[wordIndex-1] && encoded[wordIndex]))
624 result ~= ' ';
625 result ~= word;
626 }
627
628 try
629 {
630 import std.utf;
631 validate(result);
632 }
633 catch (Exception e)
634 result = toUtf8(cast(ascii)result, "ISO-8859-1", true);
635
636 return result;
637 }
638
639 /// Encodes an UTF-8 string to be used in headers.
640 string encodeRfc1522(string str)
641 {
642 if (!str.hasHighAsciiChars())
643 return str;
644
645 string[] words;
646 bool wasIntl = false;
647 foreach (word; str.split(" "))
648 {
649 bool isIntl = word.hasHighAsciiChars();
650 if (wasIntl && isIntl)
651 words[$-1] ~= " " ~ word;
652 else
653 words ~= word;
654 wasIntl = isIntl;
655 }
656
657 enum CHUNK_LENGTH_THRESHOLD = 20;
658
659 foreach (ref word; words)
660 {
661 if (!word.hasHighAsciiChars())
662 continue;
663 string[] output;
664 string s = word;
665 while (s.length)
666 {
667 size_t ptr = 0;
668 while (ptr < s.length && ptr < CHUNK_LENGTH_THRESHOLD)
669 ptr += stride(s, ptr);
670 output ~= encodeRfc1522Chunk(s[0..ptr]);
671 s = s[ptr..$];
672 }
673 word = output.join(" ");
674 }
675 return words.join(" ");
676 }
677
678 string encodeRfc1522Chunk(string str) pure
679 {
680 auto result = "=?UTF-8?B?" ~ Base64.encode(cast(ubyte[])str) ~ "?=";
681 return result;
682 }
683
684 unittest
685 {
686 auto text = "В лесу родилась ёлочка";
687 assert(decodeRfc1522(encodeRfc1522(text)) == text);
688
689 // Make sure email address isn't mangled
690 assert(encodeRfc1522("Sönke Martin <msonke@example.org>").endsWith(" <msonke@example.org>"));
691 }
692
693 string decodeQuotedPrintable(string s, bool inHeaders)
694 {
695 auto r = appender!string();
696 for (int i=0; i<s.length; )
697 if (s[i]=='=')
698 {
699 if (i+1 >= s.length || s[i+1] == '\n')
700 i+=2; // escape newline
701 else
702 r.put(cast(char)to!ubyte(s[i+1..i+3], 16)), i+=3;
703 }
704 else
705 if (s[i]=='_' && inHeaders)
706 r.put(' '), i++;
707 else
708 r.put(s[i++]);
709 return r.data;
710 }
711
712 string guessDefaultEncoding(string userAgent)
713 {
714 switch (userAgent)
715 {
716 case "DFeed":
717 // Early DFeed versions did not specify the encoding
718 return "utf8";
719 default:
720 return "windows1252";
721 }
722 }
723
724 // https://issues.dlang.org/show_bug.cgi?id=7016
725 static import ae.sys.cmd;
726
727 string decodeEncodedText(ascii s, string textEncoding)
728 {
729 try
730 return toUtf8(s, textEncoding, false);
731 catch (Exception e)
732 {
733 debug(RFC850) stderr.writefln("iconv fallback for %s (%s)", textEncoding, e.msg);
734 try
735 {
736 import ae.sys.cmd;
737 return iconv(s, textEncoding);
738 }
739 catch (Exception e)
740 {
741 debug(RFC850) stderr.writefln("ISO-8859-1 fallback (%s)", e.msg);
742 return toUtf8(s, "ISO-8859-1", false);
743 }
744 }
745 }
746
747 string decodeTransferEncoding(string data, string encoding)
748 {
749 switch (toLower(encoding))
750 {
751 case "7bit":
752 return data;
753 case "quoted-printable":
754 return decodeQuotedPrintable(data, false);
755 case "base64":
756 //return cast(string)Base64.decode(data.replace("\n", ""));
757 {
758 auto s = data.fastReplace("\n", "");
759 scope(failure) debug(RFC850) stderr.writeln(s);
760 return cast(string)Base64.decode(s);
761 }
762 default:
763 return data;
764 }
765 }
766
767 ubyte[] uudecode(string[] lines)
768 {
769 // TODO: optimize
770 //auto data = appender!(ubyte[]); // OPTLINK says no
771 ubyte[] data;
772 foreach (line; lines)
773 {
774 if (!line.length || line.startsWith("`"))
775 continue;
776 ubyte len = to!ubyte(line[0] - 32);
777 line = line[1..$];
778 while (line.length % 4)
779 line ~= 32;
780 ubyte[] lineData;
781 while (line.length)
782 {
783 uint v = 0;
784 foreach (c; line[0..4])
785 if (c == '`') // same as space
786 v <<= 6;
787 else
788 {
789 enforce(c >= 32 && c < 96, [c]);
790 v = (v<<6) | (c - 32);
791 }
792
793 auto a = cast(ubyte[])((&v)[0..1]);
794 lineData ~= a[2];
795 lineData ~= a[1];
796 lineData ~= a[0];
797
798 line = line[4..$];
799 }
800 while (len > lineData.length)
801 lineData ~= 0;
802 data ~= lineData[0..len];
803 }
804 return data;
805 }