wrote up documentation on how the MIME stuff works now
[squirrelmail.git] / functions / mime.php
... / ...
CommitLineData
1<?php
2 /** mime.php
3 **
4 ** This contains the functions necessary to detect and decode MIME
5 ** messages.
6 **
7 **/
8
9 $debug_mime = false;
10 $mime_php = true;
11
12 if (!isset($i18n_php))
13 include "../functions/i18n.php";
14 if (!isset($imap_php))
15 include "../functions/imap.php";
16 if (!isset($config_php))
17 include "../config/config.php";
18
19
20 /** Setting up the object that has the structure for the message **/
21
22 class msg_header {
23 /** msg_header contains generic variables for values that **/
24 /** could be in a header. **/
25
26 var $type0, $type1, $boundary, $charset, $encoding;
27 var $to, $from, $date, $cc, $bcc, $reply_to, $subject;
28 var $id, $mailbox, $description;
29 var $entity_id;
30 }
31
32 class message {
33 /** message is the object that contains messages. It is a recursive
34 object in that through the $entities variable, it can contain
35 more objects of type message. See documentation in mime.txt for
36 a better description of how this works.
37 **/
38 var $header;
39 var $entities;
40
41 function addEntity ($msg) {
42 $this->entities[count($this->entities)] = $msg;
43 }
44 }
45
46
47
48 /* --------------------------------------------------------------------------------- */
49 /* MIME DECODING */
50 /* --------------------------------------------------------------------------------- */
51
52 /** This function gets the structure of a message and stores it in the "message" class.
53 It will return this object for use with all relevant header information and
54 fully parsed into the standard "message" object format.
55 **/
56 function mime_structure ($imap_stream, $header) {
57 global $debug_mime;
58 sqimap_messages_flag ($imap_stream, $header->id, $header->id, "Seen");
59
60 $id = $header->id;
61 fputs ($imap_stream, "a001 FETCH $id BODYSTRUCTURE\r\n");
62 $read = fgets ($imap_stream, 10000);
63 $read = strtolower($read);
64
65 if ($debug_mime) echo "<tt>$read</tt><br><br>";
66 // isolate the body structure and remove beginning and end parenthesis
67 $read = trim(substr ($read, strpos($read, "bodystructure") + 13));
68 $read = trim(substr ($read, 0, -1));
69 $end = mime_match_parenthesis(0, $read);
70 while ($end == strlen($read)-1) {
71 $read = trim(substr ($read, 0, -1));
72 $read = trim(substr ($read, 1));
73 $end = mime_match_parenthesis(0, $read);
74 }
75
76 if ($debug_mime) echo "<tt>$read</tt><br><br>";
77
78 $msg = mime_parse_structure ($read);
79 $msg->header = $header;
80 return $msg;
81 }
82
83 function mime_parse_structure ($structure, $ent_id) {
84 global $debug_mime;
85 if ($debug_mime) echo "<font color=008800><tt>START: mime_parse_structure()</tt></font><br>";
86 $msg = new message();
87 if (substr($structure, 0, 1) == "(") {
88 $ent_id = mime_new_element_level($ent_id);
89 $start = $end = -1;
90 if ($debug_mime) echo "<br><font color=0000aa><tt>$structure</tt></font><br>";
91 do {
92 if ($debug_mime) echo "<font color=008800><tt>Found entity...</tt></font><br>";
93 $start = $end+1;
94 $end = mime_match_parenthesis ($start, $structure);
95
96 $element = substr($structure, $start+1, ($end - $start)-1);
97 $ent_id = mime_increment_id ($ent_id);
98 $newmsg = mime_parse_structure ($element, $ent_id);
99 $msg->addEntity ($newmsg);
100 } while (substr($structure, $end+1, 1) == "(");
101 } else {
102 // parse the elements
103 if ($debug_mime) echo "<br><font color=0000aa><tt>$structure</tt></font><br>";
104 $msg = mime_get_element (&$structure, $msg, $ent_id);
105 if ($debug_mime) echo "<br>";
106 }
107 return $msg;
108 if ($debug_mime) echo "<font color=008800><tt>&nbsp;&nbsp;END: mime_parse_structure()</tt></font><br>";
109 }
110
111 // Increments the element ID. An element id can look like any of
112 // the following: 1, 1.2, 4.3.2.4.1, etc. This function increments
113 // the last number of the element id, changing 1.2 to 1.3.
114 function mime_increment_id ($id) {
115 global $debug_mime;
116 if (strpos($id, ".")) {
117 $first = substr($id, 0, strrpos($id, "."));
118 $last = substr($id, strrpos($id, ".")+1);
119 $last++;
120 $new = $first . "." .$last;
121 } else {
122 $new = $id + 1;
123 }
124 if ($debug_mime) echo "<b>INCREMENT: $new</b><br>";
125 return $new;
126 }
127
128 // See comment for mime_increment_id().
129 // This adds another level on to the entity_id changing 1.3 to 1.3.0
130 // NOTE: 1.3.0 is not a valid element ID. It MUST be incremented
131 // before it can be used. I left it this way so as not to have
132 // to make a special case if it is the first entity_id. It
133 // always increments it, and that works fine.
134 function mime_new_element_level ($id) {
135 if (!$id) $id = 0;
136 else $id = $id . ".0";
137
138 return $id;
139 }
140
141 function mime_get_element (&$structure, $msg, $ent_id) {
142 global $debug_mime;
143 $elem_num = 1;
144 $msg->header = new msg_header();
145 $msg->header->entity_id = $ent_id;
146
147 while (strlen($structure) > 0) {
148 $structure = trim($structure);
149 $char = substr($structure, 0, 1);
150
151 if (substr($structure, 0, 3) == "nil") {
152 $text = "";
153 $structure = substr($structure, 3);
154 } else if ($char == "\"") {
155 // loop through until we find the matching quote, and return that as a string
156 $pos = 1;
157 $char = substr($structure, $pos, 1);
158 while ($char != "\"" && $pos < strlen($structure)) {
159 $text .= $char;
160 $pos++;
161 $char = substr($structure, $pos, 1);
162 }
163 $structure = substr($structure, strlen($text) + 2);
164 } else if ($char == "(") {
165 // comment me
166 $end = mime_match_parenthesis (0, $structure);
167 $sub = substr($structure, 1, $end-1);
168 $properties = mime_get_props($properties, $sub);
169 $structure = substr($structure, strlen($sub) + 2);
170 } else {
171 // loop through until we find a space or an end parenthesis
172 $pos = 0;
173 $char = substr($structure, $pos, 1);
174 while ($char != " " && $char != ")" && $pos < strlen($structure)) {
175 $text .= $char;
176 $pos++;
177 $char = substr($structure, $pos, 1);
178 }
179 $structure = substr($structure, strlen($text));
180 }
181 if ($debug_mime) echo "<tt>$elem_num : $text</tt><br>";
182
183 // This is where all the text parts get put into the header
184 switch ($elem_num) {
185 case 1:
186 $msg->header->type0 = $text;
187 if ($debug_mime) echo "<tt>type0 = $text</tt><br>";
188 break;
189 case 2:
190 $msg->header->type1 = $text;
191 if ($debug_mime) echo "<tt>type1 = $text</tt><br>";
192 break;
193 case 5:
194 $msg->header->description = $text;
195 if ($debug_mime) echo "<tt>description = $text</tt><br>";
196 break;
197 case 6:
198 $msg->header->encoding = $text;
199 if ($debug_mime) echo "<tt>encoding = $text</tt><br>";
200 break;
201 case 7:
202 $msg->header->size = $text;
203 if ($debug_mime) echo "<tt>size = $text</tt><br>";
204 break;
205 default:
206 if ($msg->header->type0 == "text" && $elem_num == 8) {
207 $msg->header->num_lines = $text;
208 if ($debug_mime) echo "<tt>num_lines = $text</tt><br>";
209 } else if ($msg->header->type0 == "message" && $msg->header->type1 == "rfc822" && $elem_num == 8) {
210 // This is an encapsulated message, so lets start all over again and
211 // parse this message adding it on to the existing one.
212 $structure = trim($structure);
213 if (substr($structure, 0, 1) == "(") {
214 $e = mime_match_parenthesis (0, $structure);
215 $structure = substr($structure, 0, $e);
216 $structure = substr($structure, 1);
217 $m = mime_parse_structure($structure, $msg->header->entity_id);
218 if (substr($structure, 1, 1) != "(")
219 $m->header->entity_id = mime_increment_id(mime_new_element_level($ent_id));
220 if ($m->entities) {
221 for ($i=0; $i < count($m->entities); $i++) {
222 //echo "<big>TYPE: $i - ".$m->entities[$i]->header->type0." - ".$m->entities[$i]->header->type1."</big><br>";
223 $msg->addEntity($m->entities[$i]);
224 }
225 } else {
226 //echo "<big>TYPE: ".$m->header->type0." - ".$m->header->type1."</big><br>";
227 $msg->addEntity($m);
228 }
229 $structure = "";
230 }
231 }
232 break;
233 }
234 $elem_num++;
235 $text = "";
236 }
237 // loop through the additional properties and put those in the various headers
238 if ($msg->header->type0 != "message") {
239 for ($i=0; $i < count($properties); $i++) {
240 $msg->header->{$properties[$i]["name"]} = $properties[$i]["value"];
241 if ($debug_mime) echo "<tt>".$properties[$i]["name"]." = " . $properties[$i]["value"] . "</tt><br>";
242 }
243 }
244 return $msg;
245 }
246
247 // I did most of the MIME stuff yesterday (June 20, 2000), but I couldn't
248 // figure out how to do this part, so I decided to go to bed. I woke up
249 // in the morning and had a flash of insight. I went to the white-board
250 // and scribbled it out, then spent a bit programming it, and this is the
251 // result. Nothing complicated, but I think my brain was fried yesterday.
252 //
253 // This gets properties in a nested parenthesisized list. For example,
254 // this would get passed something like: ("attachment" ("filename" "luke.tar.gz"))
255 // This returns an array called $props with all paired up properties.
256 // It ignores the "attachment" for now, maybe that should change later
257 // down the road. In this case, what is returned is:
258 // $props[0]["name"] = "filename";
259 // $props[0]["value"] = "luke.tar.gz";
260 function mime_get_props ($props, $structure) {
261 global $debug_mime;
262 while (strlen($structure) > 0) {
263 $structure = trim($structure);
264 $char = substr($structure, 0, 1);
265
266 if ($char == "\"") {
267 $pos = 1;
268 $char = substr($structure, $pos, 1);
269 while ($char != "\"" && $pos < strlen($structure)) {
270 $tmp .= $char;
271 $pos++;
272 $char = substr($structure, $pos, 1);
273 }
274 $structure = trim(substr($structure, strlen($tmp) + 2));
275 $char = substr($structure, 0, 1);
276
277 if ($char == "\"") {
278 $pos = 1;
279 $char = substr($structure, $pos, 1);
280 while ($char != "\"" && $pos < strlen($structure)) {
281 $value .= $char;
282 $pos++;
283 $char = substr($structure, $pos, 1);
284 }
285 $structure = trim(substr($structure, strlen($tmp) + 2));
286
287 $k = count($props);
288 $props[$k]["name"] = $tmp;
289 $props[$k]["value"] = $value;
290 } else if ($char == "(") {
291 $end = mime_match_parenthesis (0, $structure);
292 $sub = substr($structure, 1, $end-1);
293 $props = mime_get_props($props, $sub);
294 $structure = substr($structure, strlen($sub) + 2);
295 }
296 return $props;
297 } else if ($char == "(") {
298 $end = mime_match_parenthesis (0, $structure);
299 $sub = substr($structure, 1, $end-1);
300 $props = mime_get_props($props, $sub);
301 $structure = substr($structure, strlen($sub) + 2);
302 return $props;
303 } else {
304 return $props;
305 }
306 }
307 }
308
309 // Matches parenthesis. It will return the position of the matching
310 // parenthesis in $structure. For instance, if $structure was:
311 // ("text" "plain" ("val1name", "1") nil ... )
312 // x x
313 // then this would return 42 to match up those two.
314 function mime_match_parenthesis ($pos, $structure) {
315 $char = substr($structure, $pos, 1);
316
317 // ignore all extra characters
318 while ($pos < strlen($structure)) {
319 $pos++;
320 $char = substr($structure, $pos, 1);
321 if ($char == ")") {
322 return $pos;
323 } else if ($char == "(") {
324 $pos = mime_match_parenthesis ($pos, $structure);
325 }
326 }
327 }
328
329 function mime_fetch_body ($imap_stream, $id, $ent_id) {
330 // do a bit of error correction. If we couldn't find the entity id, just guess
331 // that it is the first one. That is usually the case anyway.
332 if (!$ent_id) $ent_id = 1;
333
334 fputs ($imap_stream, "a001 FETCH $id BODY[$ent_id]\r\n");
335 $topline = fgets ($imap_stream, 1024);
336 $size = substr ($topline, strpos($topline, "{")+1);
337 $size = substr ($size, 0, strpos($size, "}"));
338 $read = fread ($imap_stream, $size);
339 return $read;
340 }
341
342 /* -[ END MIME DECODING ]----------------------------------------------------------- */
343
344
345
346 /** This is the first function called. It decides if this is a multipart
347 message or if it should be handled as a single entity
348 **/
349 function decodeMime ($body, $header) {
350 global $username, $key, $imapServerAddress, $imapPort;
351 $imap_stream = sqimap_login($username, $key, $imapServerAddress, $imapPort, 0);
352 sqimap_mailbox_select($imap_stream, $header->mailbox);
353
354 return mime_structure ($imap_stream, $header);
355 }
356
357 function listEntities ($message) {
358 if ($message) {
359 if ($message->header->entity_id)
360 echo "<tt>" . $message->header->entity_id . " : " . $message->header->type0 . "/" . $message->header->type1 . "<br>";
361 for ($i = 0; $message->entities[$i]; $i++) {
362 $msg = listEntities($message->entities[$i], $ent_id);
363 if ($msg)
364 return $msg;
365 }
366 }
367 }
368
369 function getEntity ($message, $ent_id) {
370 if ($message) {
371 if ($message->header->entity_id == $ent_id && strlen($ent_id) == strlen($message->header->entity_id)) {
372 return $message;
373 } else {
374 for ($i = 0; $message->entities[$i]; $i++) {
375 $msg = getEntity ($message->entities[$i], $ent_id);
376 if ($msg)
377 return $msg;
378 }
379 }
380 }
381 }
382
383 function findDisplayEntity ($message) {
384 if ($message) {
385 if ($message->header->type0 == "text") {
386 if ($message->header->type1 == "plain" ||
387 $message->header->type1 == "html") {
388 return $message->header->entity_id;
389 }
390 } else {
391 for ($i=0; $message->entities[$i]; $i++) {
392 return findDisplayEntity($message->entities[$i]);
393 }
394 }
395 }
396 }
397
398 /** This returns a parsed string called $body. That string can then
399 be displayed as the actual message in the HTML. It contains
400 everything needed, including HTML Tags, Attachments at the
401 bottom, etc.
402 **/
403 function formatBody($message, $color, $wrap_at) {
404 /** this if statement checks for the entity to show as the
405 primary message. To add more of them, just put them in the
406 order that is their priority.
407 **/
408 global $username, $key, $imapServerAddress, $imapPort;
409
410
411 $id = $message->header->id;
412 $urlmailbox = urlencode($message->header->mailbox);
413
414 $imap_stream = sqimap_login($username, $key, $imapServerAddress, $imapPort, 0);
415 sqimap_mailbox_select($imap_stream, $message->header->mailbox);
416
417 $ent_num = findDisplayEntity ($message);
418 $body = mime_fetch_body ($imap_stream, $id, $ent_num);
419
420 /** If there are other types that shouldn't be formatted, add
421 them here **/
422 //if ($->type1 != "html") {
423 $body = translateText($body, $wrap_at, $charset);
424 //}
425
426 $body .= "<BR><SMALL><CENTER><A HREF=\"../src/download.php?absolute_dl=true&passed_id=$id&passed_ent_id=$ent_num&mailbox=$urlmailbox\">". _("Download this as a file") ."</A></CENTER><BR></SMALL>";
427
428 /** Display the ATTACHMENTS: message if there's more than one part **/
429 if ($message->entities) {
430 $body .= "<TABLE WIDTH=100% CELLSPACING=0 CELLPADDING=4 BORDER=0><TR><TD BGCOLOR=\"$color[0]\">";
431 $body .= "<TT><B>ATTACHMENTS:</B></TT>";
432 $body .= "</TD></TR><TR><TD BGCOLOR=\"$color[0]\">";
433 $num = 0;
434
435 /** make this recurisve at some point **/
436 $body .= formatAttachments ($message, $ent_num, $message->header->mailbox, $id);
437 $body .= "</TD></TR></TABLE>";
438 }
439 return $body;
440 }
441
442 // A recursive function that returns a list of attachments with links
443 // to where to download these attachments
444 function formatAttachments ($message, $ent_id, $mailbox, $id) {
445 if ($message) {
446 if (!$message->entities) {
447 $type0 = strtolower($message->header->type0);
448 $type1 = strtolower($message->header->type1);
449
450 if ($message->header->entity_id != $ent_id) {
451 $filename = $message->header->filename;
452 if (trim($filename) == "") {
453 $display_filename = "untitled-".$message->header->entity_id;
454 } else {
455 $display_filename = $filename;
456 }
457
458 $urlMailbox = urlencode($mailbox);
459 $ent = urlencode($message->header->entity_id);
460 $body .= "<TT>&nbsp;&nbsp;&nbsp;<A HREF=\"../src/download.php?passed_id=$id&mailbox=$urlMailbox&passed_ent_id=$ent\">" . $display_filename . "</A>&nbsp;&nbsp;(TYPE: $type0/$type1)";
461 if ($message->header->description)
462 $body .= "&nbsp;&nbsp;<b>" . htmlspecialchars($message->header->description)."</b>";
463 $body .= "</TT><BR>";
464 $num++;
465 }
466 return $body;
467 } else {
468 for ($i = 0; $i < count($message->entities); $i++) {
469 $body .= formatAttachments ($message->entities[$i], $ent_id, $mailbox, $id);
470 }
471 return $body;
472 }
473 }
474 }
475
476
477 /** this function decodes the body depending on the encoding type. **/
478 function decodeBody($body, $encoding) {
479 $encoding = strtolower($encoding);
480
481 if ($encoding == "quoted-printable") {
482 $body = quoted_printable_decode($body);
483
484 while (ereg("=\n", $body))
485 $body = ereg_replace ("=\n", "", $body);
486 } else if ($encoding == "base64") {
487 $body = base64_decode($body);
488 }
489
490 // All other encodings are returned raw.
491 return $body;
492 }
493
494
495 // This functions decode strings that is encoded according to
496 // RFC1522 (MIME Part Two: Message Header Extensions for Non-ASCII Text).
497 function decodeHeader ($string) {
498 if (eregi('=\?([^?]+)\?(q|b)\?([^?]+)\?=',
499 $string, $res)) {
500 if (ucfirst($res[2]) == "B") {
501 $replace = base64_decode($res[3]);
502 } else {
503 $replace = ereg_replace("_", " ", $res[3]);
504 $replace = quoted_printable_decode($replace);
505 }
506
507 $replace = charset_decode ($res[1], $replace);
508
509 $string = eregi_replace
510 ('=\?([^?]+)\?(q|b)\?([^?]+)\?=',
511 $replace, $string);
512 // In case there should be more encoding in the string: recurse
513 return (decodeHeader($string));
514 } else
515 return ($string);
516 }
517
518 // Encode a string according to RFC 1522 for use in headers if it
519 // contains 8-bit characters
520 function encodeHeader ($string) {
521 global $default_charset;
522
523 // Encode only if the string contains 8-bit characters
524 if (ereg("[\200-\377]", $string)) {
525 $newstring = "=?$default_charset?Q?";
526 $newstring .= str_replace(" ", "_", $string);
527
528 while (ereg("([\200-\377])", $newstring, $regs)) {
529 $replace = $regs[1];
530 $insert = "=" . bin2hex($replace);
531 $newstring = str_replace($replace, $insert, $newstring);
532 }
533
534 $newstring .= "?=";
535
536 return $newstring;
537 }
538
539 return $string;
540 }
541
542?>