wrote up documentation on how the MIME stuff works now
[squirrelmail.git] / functions / mime.php
CommitLineData
59177427 1<?php
aceb0d5c 2 /** mime.php
3 **
d068c0ec 4 ** This contains the functions necessary to detect and decode MIME
5 ** messages.
6 **
aceb0d5c 7 **/
8
e79bed1b 9 $debug_mime = false;
d068c0ec 10 $mime_php = true;
aceb0d5c 11
1fd97780 12 if (!isset($i18n_php))
13 include "../functions/i18n.php";
8beafbbc 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;
ea48eb25 28 var $id, $mailbox, $description;
8beafbbc 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 }
1fd97780 45
bcb432a3 46
bcb432a3 47
8beafbbc 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) {
e79bed1b 57 global $debug_mime;
8beafbbc 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");
e79bed1b 62 $read = fgets ($imap_stream, 10000);
63 $read = strtolower($read);
8beafbbc 64
e79bed1b 65 if ($debug_mime) echo "<tt>$read</tt><br><br>";
8beafbbc 66 // isolate the body structure and remove beginning and end parenthesis
67 $read = trim(substr ($read, strpos($read, "bodystructure") + 13));
ea48eb25 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 }
8beafbbc 75
e79bed1b 76 if ($debug_mime) echo "<tt>$read</tt><br><br>";
77
8beafbbc 78 $msg = mime_parse_structure ($read);
79 $msg->header = $header;
80 return $msg;
81 }
82
83 function mime_parse_structure ($structure, $ent_id) {
e79bed1b 84 global $debug_mime;
85 if ($debug_mime) echo "<font color=008800><tt>START: mime_parse_structure()</tt></font><br>";
8beafbbc 86 $msg = new message();
87 if (substr($structure, 0, 1) == "(") {
88 $ent_id = mime_new_element_level($ent_id);
89 $start = $end = -1;
ea48eb25 90 if ($debug_mime) echo "<br><font color=0000aa><tt>$structure</tt></font><br>";
8beafbbc 91 do {
e79bed1b 92 if ($debug_mime) echo "<font color=008800><tt>Found entity...</tt></font><br>";
8beafbbc 93 $start = $end+1;
94 $end = mime_match_parenthesis ($start, $structure);
95
96 $element = substr($structure, $start+1, ($end - $start)-1);
ea48eb25 97 $ent_id = mime_increment_id ($ent_id);
8beafbbc 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
e79bed1b 103 if ($debug_mime) echo "<br><font color=0000aa><tt>$structure</tt></font><br>";
ea48eb25 104 $msg = mime_get_element (&$structure, $msg, $ent_id);
e79bed1b 105 if ($debug_mime) echo "<br>";
8beafbbc 106 }
107 return $msg;
e79bed1b 108 if ($debug_mime) echo "<font color=008800><tt>&nbsp;&nbsp;END: mime_parse_structure()</tt></font><br>";
8beafbbc 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) {
ea48eb25 115 global $debug_mime;
8beafbbc 116 if (strpos($id, ".")) {
117 $first = substr($id, 0, strrpos($id, "."));
ea48eb25 118 $last = substr($id, strrpos($id, ".")+1);
8beafbbc 119 $last++;
ea48eb25 120 $new = $first . "." .$last;
8beafbbc 121 } else {
122 $new = $id + 1;
123 }
ea48eb25 124 if ($debug_mime) echo "<b>INCREMENT: $new</b><br>";
8beafbbc 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) {
ea48eb25 135 if (!$id) $id = 0;
136 else $id = $id . ".0";
137
8beafbbc 138 return $id;
139 }
140
ea48eb25 141 function mime_get_element (&$structure, $msg, $ent_id) {
e79bed1b 142 global $debug_mime;
8beafbbc 143 $elem_num = 1;
ea48eb25 144 $msg->header = new msg_header();
145 $msg->header->entity_id = $ent_id;
8beafbbc 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);
aceb0d5c 178 }
8beafbbc 179 $structure = substr($structure, strlen($text));
aceb0d5c 180 }
e79bed1b 181 if ($debug_mime) echo "<tt>$elem_num : $text</tt><br>";
8beafbbc 182
183 // This is where all the text parts get put into the header
184 switch ($elem_num) {
185 case 1:
ea48eb25 186 $msg->header->type0 = $text;
e79bed1b 187 if ($debug_mime) echo "<tt>type0 = $text</tt><br>";
8beafbbc 188 break;
189 case 2:
ea48eb25 190 $msg->header->type1 = $text;
e79bed1b 191 if ($debug_mime) echo "<tt>type1 = $text</tt><br>";
8beafbbc 192 break;
ea48eb25 193 case 5:
194 $msg->header->description = $text;
195 if ($debug_mime) echo "<tt>description = $text</tt><br>";
196 break;
8beafbbc 197 case 6:
ea48eb25 198 $msg->header->encoding = $text;
e79bed1b 199 if ($debug_mime) echo "<tt>encoding = $text</tt><br>";
8beafbbc 200 break;
201 case 7:
ea48eb25 202 $msg->header->size = $text;
e79bed1b 203 if ($debug_mime) echo "<tt>size = $text</tt><br>";
8beafbbc 204 break;
205 default:
ea48eb25 206 if ($msg->header->type0 == "text" && $elem_num == 8) {
207 $msg->header->num_lines = $text;
e79bed1b 208 if ($debug_mime) echo "<tt>num_lines = $text</tt><br>";
ea48eb25 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 }
8beafbbc 231 }
232 break;
233 }
234 $elem_num++;
235 $text = "";
236 }
237 // loop through the additional properties and put those in the various headers
ea48eb25 238 if ($msg->header->type0 != "message") {
8beafbbc 239 for ($i=0; $i < count($properties); $i++) {
ea48eb25 240 $msg->header->{$properties[$i]["name"]} = $properties[$i]["value"];
e79bed1b 241 if ($debug_mime) echo "<tt>".$properties[$i]["name"]." = " . $properties[$i]["value"] . "</tt><br>";
8beafbbc 242 }
ea48eb25 243 }
244 return $msg;
8beafbbc 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) {
e79bed1b 261 global $debug_mime;
8beafbbc 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);
ea48eb25 302 return $props;
8beafbbc 303 } else {
304 return $props;
7831268e 305 }
8beafbbc 306 }
307 }
7831268e 308
8beafbbc 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 }
d4467150 326 }
8beafbbc 327 }
d4467150 328
8beafbbc 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");
e79bed1b 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;
d4467150 340 }
341
8beafbbc 342 /* -[ END MIME DECODING ]----------------------------------------------------------- */
d4467150 343
aceb0d5c 344
d4467150 345
8beafbbc 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
4809f489 348 **/
8beafbbc 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 }
b1dadc61 356
ea48eb25 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
8beafbbc 369 function getEntity ($message, $ent_id) {
370 if ($message) {
ea48eb25 371 if ($message->header->entity_id == $ent_id && strlen($ent_id) == strlen($message->header->entity_id)) {
8beafbbc 372 return $message;
b1dadc61 373 } else {
8beafbbc 374 for ($i = 0; $message->entities[$i]; $i++) {
375 $msg = getEntity ($message->entities[$i], $ent_id);
376 if ($msg)
377 return $msg;
b1dadc61 378 }
8beafbbc 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 }
d4467150 395 }
b1dadc61 396 }
8405ee35 397
d068c0ec 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.
4809f489 402 **/
a8648d75 403 function formatBody($message, $color, $wrap_at) {
d068c0ec 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.
4809f489 407 **/
8beafbbc 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);
8405ee35 419
d068c0ec 420 /** If there are other types that shouldn't be formatted, add
421 them here **/
8beafbbc 422 //if ($->type1 != "html") {
17ce8467 423 $body = translateText($body, $wrap_at, $charset);
8beafbbc 424 //}
78509c54 425
9f2215a1 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>";
7831268e 427
b1dadc61 428 /** Display the ATTACHMENTS: message if there's more than one part **/
8beafbbc 429 if ($message->entities) {
7831268e 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]\">";
b1dadc61 433 $num = 0;
434
8beafbbc 435 /** make this recurisve at some point **/
436 $body .= formatAttachments ($message, $ent_num, $message->header->mailbox, $id);
7831268e 437 $body .= "</TD></TR></TABLE>";
8405ee35 438 }
d4467150 439 return $body;
440 }
441
8beafbbc 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) == "") {
ea48eb25 453 $display_filename = "untitled-".$message->header->entity_id;
8beafbbc 454 } else {
455 $display_filename = $filename;
456 }
457
458 $urlMailbox = urlencode($mailbox);
459 $ent = urlencode($message->header->entity_id);
ea48eb25 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>";
8beafbbc 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 }
4809f489 475
476
477 /** this function decodes the body depending on the encoding type. **/
d4467150 478 function decodeBody($body, $encoding) {
479 $encoding = strtolower($encoding);
7831268e 480
ef3f274f 481 if ($encoding == "quoted-printable") {
482 $body = quoted_printable_decode($body);
db87f79c 483
ef3f274f 484 while (ereg("=\n", $body))
485 $body = ereg_replace ("=\n", "", $body);
97be2168 486 } else if ($encoding == "base64") {
ef3f274f 487 $body = base64_decode($body);
d4467150 488 }
ef3f274f 489
490 // All other encodings are returned raw.
491 return $body;
aceb0d5c 492 }
a4c2cd49 493
494
495 // This functions decode strings that is encoded according to
496 // RFC1522 (MIME Part Two: Message Header Extensions for Non-ASCII Text).
2e434774 497 function decodeHeader ($string) {
1fd97780 498 if (eregi('=\?([^?]+)\?(q|b)\?([^?]+)\?=',
a4c2cd49 499 $string, $res)) {
1fd97780 500 if (ucfirst($res[2]) == "B") {
501 $replace = base64_decode($res[3]);
a4c2cd49 502 } else {
1fd97780 503 $replace = ereg_replace("_", " ", $res[3]);
a4c2cd49 504 $replace = quoted_printable_decode($replace);
505 }
506
1fd97780 507 $replace = charset_decode ($res[1], $replace);
a4c2cd49 508
509 $string = eregi_replace
1fd97780 510 ('=\?([^?]+)\?(q|b)\?([^?]+)\?=',
a4c2cd49 511 $replace, $string);
2e434774 512 // In case there should be more encoding in the string: recurse
513 return (decodeHeader($string));
a4c2cd49 514 } else
515 return ($string);
516 }
517
c3084273 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
9f9d7d28 542?>