added debugging information and fixed a minor bug -- not reading in enough
[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;
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 }
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));
68 $read = trim(substr ($read, 0, -2));
69 $read = trim(substr ($read, 1));
70
e79bed1b 71 if ($debug_mime) echo "<tt>$read</tt><br><br>";
72
8beafbbc 73 $msg = mime_parse_structure ($read);
74 $msg->header = $header;
75 return $msg;
76 }
77
78 function mime_parse_structure ($structure, $ent_id) {
e79bed1b 79 global $debug_mime;
80 if ($debug_mime) echo "<font color=008800><tt>START: mime_parse_structure()</tt></font><br>";
8beafbbc 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 {
e79bed1b 86 if ($debug_mime) echo "<font color=008800><tt>Found entity...</tt></font><br>";
8beafbbc 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
e79bed1b 97 if ($debug_mime) echo "<br><font color=0000aa><tt>$structure</tt></font><br>";
8beafbbc 98 $msg->header = new msg_header();
99 $msg->header = mime_get_element (&$structure, $header);
100 $msg->header->entity_id = $ent_id;
e79bed1b 101 if ($debug_mime) echo "<br>";
8beafbbc 102 }
103 return $msg;
e79bed1b 104 if ($debug_mime) echo "<font color=008800><tt>&nbsp;&nbsp;END: mime_parse_structure()</tt></font><br>";
8beafbbc 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) {
e79bed1b 137 global $debug_mime;
8beafbbc 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);
aceb0d5c 171 }
8beafbbc 172 $structure = substr($structure, strlen($text));
aceb0d5c 173 }
e79bed1b 174 if ($debug_mime) echo "<tt>$elem_num : $text</tt><br>";
8beafbbc 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;
e79bed1b 180 if ($debug_mime) echo "<tt>type0 = $text</tt><br>";
8beafbbc 181 break;
182 case 2:
183 $header->type1 = $text;
e79bed1b 184 if ($debug_mime) echo "<tt>type1 = $text</tt><br>";
8beafbbc 185 break;
186 case 6:
187 $header->encoding = $text;
e79bed1b 188 if ($debug_mime) echo "<tt>encoding = $text</tt><br>";
8beafbbc 189 break;
190 case 7:
191 $header->size = $text;
e79bed1b 192 if ($debug_mime) echo "<tt>size = $text</tt><br>";
8beafbbc 193 break;
194 default:
195 if ($header->type0 == "text" && $elem_num == 8) {
196 $header->num_lines = $text;
e79bed1b 197 if ($debug_mime) echo "<tt>num_lines = $text</tt><br>";
8beafbbc 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"];
e79bed1b 207 if ($debug_mime) echo "<tt>".$properties[$i]["name"]." = " . $properties[$i]["value"] . "</tt><br>";
8beafbbc 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) {
e79bed1b 226 global $debug_mime;
8beafbbc 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;
7831268e 269 }
8beafbbc 270 }
271 }
7831268e 272
8beafbbc 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 }
d4467150 290 }
8beafbbc 291 }
d4467150 292
8beafbbc 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");
e79bed1b 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;
d4467150 304 }
305
8beafbbc 306 /* -[ END MIME DECODING ]----------------------------------------------------------- */
d4467150 307
aceb0d5c 308
d4467150 309
8beafbbc 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
4809f489 312 **/
8beafbbc 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 }
b1dadc61 320
8beafbbc 321 function getEntity ($message, $ent_id) {
322 if ($message) {
323 if ($message->header->entity_id == $ent_id) {
324 return $message;
b1dadc61 325 } else {
8beafbbc 326 for ($i = 0; $message->entities[$i]; $i++) {
327 $msg = getEntity ($message->entities[$i], $ent_id);
328 if ($msg)
329 return $msg;
b1dadc61 330 }
8beafbbc 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 }
d4467150 347 }
b1dadc61 348 }
8405ee35 349
d068c0ec 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.
4809f489 354 **/
a8648d75 355 function formatBody($message, $color, $wrap_at) {
d068c0ec 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.
4809f489 359 **/
8beafbbc 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);
8405ee35 371
d068c0ec 372 /** If there are other types that shouldn't be formatted, add
373 them here **/
8beafbbc 374 //if ($->type1 != "html") {
17ce8467 375 $body = translateText($body, $wrap_at, $charset);
8beafbbc 376 //}
78509c54 377
9f2215a1 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>";
7831268e 379
b1dadc61 380 /** Display the ATTACHMENTS: message if there's more than one part **/
8beafbbc 381 if ($message->entities) {
7831268e 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]\">";
b1dadc61 385 $num = 0;
386
8beafbbc 387 /** make this recurisve at some point **/
388 $body .= formatAttachments ($message, $ent_num, $message->header->mailbox, $id);
7831268e 389 $body .= "</TD></TR></TABLE>";
8405ee35 390 }
d4467150 391 return $body;
392 }
393
8beafbbc 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 }
4809f489 424
425
426 /** this function decodes the body depending on the encoding type. **/
d4467150 427 function decodeBody($body, $encoding) {
428 $encoding = strtolower($encoding);
7831268e 429
ef3f274f 430 if ($encoding == "quoted-printable") {
431 $body = quoted_printable_decode($body);
db87f79c 432
ef3f274f 433 while (ereg("=\n", $body))
434 $body = ereg_replace ("=\n", "", $body);
97be2168 435 } else if ($encoding == "base64") {
ef3f274f 436 $body = base64_decode($body);
d4467150 437 }
ef3f274f 438
439 // All other encodings are returned raw.
440 return $body;
aceb0d5c 441 }
a4c2cd49 442
443
444 // This functions decode strings that is encoded according to
445 // RFC1522 (MIME Part Two: Message Header Extensions for Non-ASCII Text).
2e434774 446 function decodeHeader ($string) {
1fd97780 447 if (eregi('=\?([^?]+)\?(q|b)\?([^?]+)\?=',
a4c2cd49 448 $string, $res)) {
1fd97780 449 if (ucfirst($res[2]) == "B") {
450 $replace = base64_decode($res[3]);
a4c2cd49 451 } else {
1fd97780 452 $replace = ereg_replace("_", " ", $res[3]);
a4c2cd49 453 $replace = quoted_printable_decode($replace);
454 }
455
1fd97780 456 $replace = charset_decode ($res[1], $replace);
a4c2cd49 457
458 $string = eregi_replace
1fd97780 459 ('=\?([^?]+)\?(q|b)\?([^?]+)\?=',
a4c2cd49 460 $replace, $string);
2e434774 461 // In case there should be more encoding in the string: recurse
462 return (decodeHeader($string));
a4c2cd49 463 } else
464 return ($string);
465 }
466
c3084273 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
9f9d7d28 491?>