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