small fix
[squirrelmail.git] / functions / mime.php
CommitLineData
59177427 1<?php
2ba13803 2
35586184 3/**
4 * mime.php
5 *
15e6162e 6 * Copyright (c) 1999-2002 The SquirrelMail Project Team
35586184 7 * Licensed under the GNU GPL. For full terms see the file COPYING.
8 *
9 * This contains the functions necessary to detect and decode MIME
10 * messages.
11 *
12 * $Id$
13 */
b74ba498 14
35586184 15require_once('../functions/imap.php');
16require_once('../functions/attachment_common.php');
8beafbbc 17
35586184 18/** Setting up the objects that have the structure for the message **/
19class msg_header {
20 /** msg_header contains generic variables for values that **/
21 /** could be in a header. **/
b74ba498 22
35586184 23 var $type0 = '', $type1 = '', $boundary = '', $charset = '';
24 var $encoding = '', $size = 0, $to = array(), $from = '', $date = '';
25 var $cc = array(), $bcc = array(), $reply_to = '', $subject = '';
26 var $id = 0, $mailbox = '', $description = '', $filename = '';
27 var $entity_id = 0, $message_id = 0, $name = '';
28 // var $priority = "";
29}
b74ba498 30
451f74a2 31class message {
32 /** message is the object that contains messages. It is a recursive
33 object in that through the $entities variable, it can contain
34 more objects of type message. See documentation in mime.txt for
35 a better description of how this works.
36 **/
37 var $header = '';
38 var $entities = array();
39
40 function addEntity ($msg) {
41 $this->entities[] = $msg;
42 }
43}
8beafbbc 44
451f74a2 45/* --------------------------------------------------------------------------------- */
46/* MIME DECODING */
47/* --------------------------------------------------------------------------------- */
b74ba498 48
451f74a2 49/* This function gets the structure of a message and stores it in the "message" class.
50 * It will return this object for use with all relevant header information and
51 * fully parsed into the standard "message" object format.
52 */
53function mime_structure ($imap_stream, $header) {
54
55 sqimap_messages_flag ($imap_stream, $header->id, $header->id, 'Seen');
56 $ssid = sqimap_session_id();
57 $lsid = strlen( $ssid );
58 $id = $header->id;
59 fputs ($imap_stream, "$ssid FETCH $id BODYSTRUCTURE\r\n");
60 //
61 // This should use sqimap_read_data instead of reading it itself
62 //
63 $read = fgets ($imap_stream, 10000);
64 $bodystructure = '';
65 while ( substr($read, 0, $lsid) <> $ssid &&
66 !feof( $imap_stream ) ) {
67 $bodystructure .= $read;
68 $read = fgets ($imap_stream, 10000);
69 }
70 $read = $bodystructure;
71
72 // isolate the body structure and remove beginning and end parenthesis
73 $read = trim(substr ($read, strpos(strtolower($read), 'bodystructure') + 13));
74 $read = trim(substr ($read, 0, -1));
75 $end = mime_match_parenthesis(0, $read);
76 while ($end == strlen($read)-1) {
77 $read = trim(substr ($read, 0, -1));
78 $read = trim(substr ($read, 1));
79 $end = mime_match_parenthesis(0, $read);
80 }
81
82 $msg = mime_parse_structure ($read, 0);
83 $msg->header = $header;
84
85 return( $msg );
86}
b74ba498 87
451f74a2 88/* this starts the parsing of a particular structure. It is called recursively,
89 * so it can be passed different structures. It returns an object of type
90 * $message.
91 * First, it checks to see if it is a multipart message. If it is, then it
92 * handles that as it sees is necessary. If it is just a regular entity,
93 * then it parses it and adds the necessary header information (by calling out
94 * to mime_get_elements()
95 */
96function mime_parse_structure ($structure, $ent_id) {
97
98 $msg = new message();
99 if ($structure{0} == '(') {
100 $ent_id = mime_new_element_level($ent_id);
101 $start = $end = -1;
102 do {
103 $start = $end+1;
104 $end = mime_match_parenthesis ($start, $structure);
105
106 $element = substr($structure, $start+1, ($end - $start)-1);
107 $ent_id = mime_increment_id ($ent_id);
108 $newmsg = mime_parse_structure ($element, $ent_id);
109 $msg->addEntity ($newmsg);
110 } while ($structure{$end+1} == '(');
111 } else {
112 // parse the elements
113 $msg = mime_get_element ($structure, $msg, $ent_id);
114 }
115 return $msg;
116}
e4a256af 117
451f74a2 118/* Increments the element ID. An element id can look like any of
119 * the following: 1, 1.2, 4.3.2.4.1, etc. This function increments
120 * the last number of the element id, changing 1.2 to 1.3.
121 */
122function mime_increment_id ($id) {
123
124 if (strpos($id, ".")) {
125 $first = substr($id, 0, strrpos($id, "."));
126 $last = substr($id, strrpos($id, ".")+1);
127 $last++;
128 $new = $first . "." .$last;
129 } else {
130 $new = $id + 1;
131 }
132
133 return $new;
134}
135
136/*
137 * See comment for mime_increment_id().
138 * This adds another level on to the entity_id changing 1.3 to 1.3.0
139 * NOTE: 1.3.0 is not a valid element ID. It MUST be incremented
140 * before it can be used. I left it this way so as not to have
141 * to make a special case if it is the first entity_id. It
142 * always increments it, and that works fine.
143 */
144function mime_new_element_level ($id) {
145
146 if (!$id) {
147 $id = 0;
148 } else {
149 $id = $id . '.0';
150 }
151
152 return( $id );
153}
154
155function mime_get_element (&$structure, $msg, $ent_id) {
156
157 $elem_num = 1;
158 $msg->header = new msg_header();
159 $msg->header->entity_id = $ent_id;
160 $properties = array();
161
162 while (strlen($structure) > 0) {
163 $structure = trim($structure);
164 $char = $structure{0};
165
166 if (strtolower(substr($structure, 0, 3)) == 'nil') {
167 $text = '';
168 $structure = substr($structure, 3);
169 } else if ($char == '"') {
170 // loop through until we find the matching quote, and return that as a string
171 $pos = 1;
172 $text = '';
173 while ( ($char = $structure{$pos} ) <> '"' && $pos < strlen($structure)) {
174 $text .= $char;
175 $pos++;
176 }
177 $structure = substr($structure, strlen($text) + 2);
178 } else if ($char == '(') {
179 // comment me
180 $end = mime_match_parenthesis (0, $structure);
181 $sub = substr($structure, 1, $end-1);
182 $properties = mime_get_props($properties, $sub);
183 $structure = substr($structure, strlen($sub) + 2);
184 } else {
185 // loop through until we find a space or an end parenthesis
186 $pos = 0;
187 $char = $structure{$pos};
188 $text = '';
189 while ($char != ' ' && $char != ')' && $pos < strlen($structure)) {
190 $text .= $char;
191 $pos++;
192 $char = $structure{$pos};
193 }
194 $structure = substr($structure, strlen($text));
195 }
196
197 // This is where all the text parts get put into the header
198 switch ($elem_num) {
199 case 1:
200 $msg->header->type0 = strtolower($text);
201 break;
202 case 2:
203 $msg->header->type1 = strtolower($text);
204 break;
205 case 4: // Id
206 // Invisimail enclose images with <>
207 $msg->header->id = str_replace( '<', '', str_replace( '>', '', $text ) );
208 break;
209 case 5:
210 $msg->header->description = $text;
211 break;
212 case 6:
213 $msg->header->encoding = strtolower($text);
214 break;
215 case 7:
216 $msg->header->size = $text;
217 break;
218 default:
219 if ($msg->header->type0 == 'text' && $elem_num == 8) {
220 // This is a plain text message, so lets get the number of lines
221 // that it contains.
222 $msg->header->num_lines = $text;
223
224 } else if ($msg->header->type0 == 'message' && $msg->header->type1 == 'rfc822' && $elem_num == 8) {
225 // This is an encapsulated message, so lets start all over again and
226 // parse this message adding it on to the existing one.
227 $structure = trim($structure);
228 if ( $structure{0} == '(' ) {
229 $e = mime_match_parenthesis (0, $structure);
230 $structure = substr($structure, 0, $e);
231 $structure = substr($structure, 1);
232 $m = mime_parse_structure($structure, $msg->header->entity_id);
233
234 // the following conditional is there to correct a bug that wasn't
235 // incrementing the entity IDs correctly because of the special case
236 // that message/rfc822 is. This fixes it fine.
237 if (substr($structure, 1, 1) != '(')
238 $m->header->entity_id = mime_increment_id(mime_new_element_level($ent_id));
239
240 // Now we'll go through and reformat the results.
241 if ($m->entities) {
242 for ($i=0; $i < count($m->entities); $i++) {
243 $msg->addEntity($m->entities[$i]);
244 }
245 } else {
246 $msg->addEntity($m);
247 }
248 $structure = "";
249 }
250 }
251 break;
252 }
253 $elem_num++;
254 $text = "";
255 }
256 // loop through the additional properties and put those in the various headers
257 if ($msg->header->type0 != 'message') {
258 for ($i=0; $i < count($properties); $i++) {
259 $msg->header->{$properties[$i]['name']} = $properties[$i]['value'];
260 }
261 }
262
263 return $msg;
264}
265
266/*
267 * I did most of the MIME stuff yesterday (June 20, 2000), but I couldn't
268 * figure out how to do this part, so I decided to go to bed. I woke up
269 * in the morning and had a flash of insight. I went to the white-board
270 * and scribbled it out, then spent a bit programming it, and this is the
271 * result. Nothing complicated, but I think my brain was fried yesterday.
272 * Funny how that happens some times.
273 *
274 * This gets properties in a nested parenthesisized list. For example,
275 * this would get passed something like: ("attachment" ("filename" "luke.tar.gz"))
276 * This returns an array called $props with all paired up properties.
277 * It ignores the "attachment" for now, maybe that should change later
278 * down the road. In this case, what is returned is:
279 * $props[0]["name"] = "filename";
280 * $props[0]["value"] = "luke.tar.gz";
281 */
282function mime_get_props ($props, $structure) {
283
284 while (strlen($structure) > 0) {
285 $structure = trim($structure);
286 $char = $structure{0};
287
288 if ($char == '"') {
289 $pos = 1;
290 $tmp = '';
291 while ( ( $char = $structure{$pos} ) != '"' &&
292 $pos < strlen($structure)) {
293 $tmp .= $char;
294 $pos++;
295 }
296 $structure = trim(substr($structure, strlen($tmp) + 2));
297 $char = $structure{0};
298
299 if ($char == '"') {
300 $pos = 1;
301 $value = '';
302 while ( ( $char = $structure{$pos} ) != '"' &&
303 $pos < strlen($structure) ) {
304 $value .= $char;
305 $pos++;
306 }
307 $structure = trim(substr($structure, strlen($tmp) + 2));
308
309 $k = count($props);
310 $props[$k]['name'] = strtolower($tmp);
311 $props[$k]['value'] = $value;
312 } else if ($char == '(') {
313 $end = mime_match_parenthesis (0, $structure);
314 $sub = substr($structure, 1, $end-1);
315 if (! isset($props))
316 $props = array();
317 $props = mime_get_props($props, $sub);
318 $structure = substr($structure, strlen($sub) + 2);
319 }
320 return $props;
321 } else if ($char == '(') {
322 $end = mime_match_parenthesis (0, $structure);
323 $sub = substr($structure, 1, $end-1);
324 $props = mime_get_props($props, $sub);
325 $structure = substr($structure, strlen($sub) + 2);
326 return $props;
327 } else {
328 return $props;
329 }
330 }
331}
332
333/*
334 * Matches parenthesis. It will return the position of the matching
335 * parenthesis in $structure. For instance, if $structure was:
336 * ("text" "plain" ("val1name", "1") nil ... )
337 * x x
338 * then this would return 42 to match up those two.
339 */
340function mime_match_parenthesis ($pos, $structure) {
341
342 $j = strlen( $structure );
343
344 // ignore all extra characters
345 // If inside of a string, skip string -- Boundary IDs and other
346 // things can have ) in them.
347 if ( $structure{$pos} != '(' ) {
348 return( $j );
349 }
350
351 while ( $pos < $j ) {
352 $pos++;
353 if ($structure{$pos} == ')') {
8beafbbc 354 return $pos;
451f74a2 355 } elseif ($structure{$pos} == '"') {
b74ba498 356 $pos++;
451f74a2 357 while ( $structure{$pos} != '"' &&
358 $pos < $j ) {
359 if (substr($structure, $pos, 2) == '\\"') {
b74ba498 360 $pos++;
451f74a2 361 } elseif (substr($structure, $pos, 2) == '\\\\') {
b74ba498 362 $pos++;
451f74a2 363 }
b74ba498 364 $pos++;
5ffe5a7e 365 }
451f74a2 366 } elseif ( $structure{$pos} == '(' ) {
8beafbbc 367 $pos = mime_match_parenthesis ($pos, $structure);
451f74a2 368 }
369 }
370 echo _("Error decoding mime structure. Report this as a bug!") . '<br>';
371 return( $pos );
372}
373
374function mime_fetch_body ($imap_stream, $id, $ent_id ) {
375 // do a bit of error correction. If we couldn't find the entity id, just guess
376 // that it is the first one. That is usually the case anyway.
377 if (!$ent_id)
378 $ent_id = 1;
ef22bc84 379 $data = sqimap_run_command ($imap_stream, "FETCH $id BODY[$ent_id]", true, $response, $message);
451f74a2 380 $topline = array_shift($data);
381 while (! ereg('\\* [0-9]+ FETCH ', $topline) && $data)
a3daaaf3 382 $topline = array_shift($data);
451f74a2 383 $wholemessage = implode('', $data);
384 if (ereg('\\{([^\\}]*)\\}', $topline, $regs)) {
385 $ret = substr( $wholemessage, 0, $regs[1] );
386 /*
387 There is some information in the content info header that could be important
388 in order to parse html messages. Let's get them here.
389 */
390 if ( $ret{0} == '<' ) {
1c72b151 391 $data = sqimap_run_command ($imap_stream, "FETCH $id BODY[$ent_id.MIME]", true, $response, $message);
451f74a2 392 $base = '';
393 $k = 10;
394 foreach( $data as $d ) {
395 if ( substr( $d, 0, 13 ) == 'Content-Base:' ) {
396 $j = strlen( $d );
397 $i = 13;
398 $base = '';
399 while ( $i < $j &&
400 ( !isNoSep( $d{$i} ) || $d{$i} == '"' ) )
401 $i++;
402 while ( $i < $j ) {
403 if ( isNoSep( $d{$i} ) )
404 $base .= $d{$i};
405 $i++;
a3daaaf3 406 }
451f74a2 407 $k = 0;
408 } elseif ( $k == 1 && !isnosep( $d{0} ) ) {
409 $base .= substr( $d, 1 );
a3daaaf3 410 }
451f74a2 411 $k++;
a3daaaf3 412 }
451f74a2 413 if ( $base <> '' )
494424e3 414
451f74a2 415 $ret = "<base href=\"$base\">" . $ret;
416 }
417 } else if (ereg('"([^"]*)"', $topline, $regs)) {
418 $ret = $regs[1];
419 } else {
420 global $where, $what, $mailbox, $passed_id, $startMessage;
421 $par = "mailbox=".urlencode($mailbox)."&passed_id=$passed_id";
422 if (isset($where) && isset($what)) {
423 $par .= "&where=".urlencode($where)."&what=".urlencode($what);
a3daaaf3 424 } else {
451f74a2 425 $par .= "&startMessage=$startMessage&show_more=0";
426 }
427 $par .= '&response='.urlencode($response).'&message='.urlencode($message).
428 '&topline='.urlencode($topline);
a019eeb8 429
451f74a2 430 echo '<b><font color=$color[2]>' .
431 _("Body retrieval error. The reason for this is most probably that the message is malformed. Please help us making future versions better by submitting this message to the developers knowledgebase!") .
432 "<A HREF=\"../src/retrievalerror.php?$par\">Submit message</A><BR>" .
433 '<tt>' . _("Response:") . "$response<BR>" .
434 _("Message:") . " $message<BR>" .
435 _("FETCH line:") . " $topline<BR></tt></font></b>";
a019eeb8 436
1c72b151 437 $data = sqimap_run_command ($imap_stream, "FETCH $passed_id BODY[]", true, $response, $message);
451f74a2 438 array_shift($data);
439 $wholemessage = implode('', $data);
a019eeb8 440
451f74a2 441 $ret = "---------------\n$wholemessage";
a019eeb8 442
a3daaaf3 443 }
451f74a2 444 return( $ret );
445}
d4467150 446
451f74a2 447function mime_print_body_lines ($imap_stream, $id, $ent_id, $encoding) {
448 // do a bit of error correction. If we couldn't find the entity id, just guess
449 // that it is the first one. That is usually the case anyway.
450 if (!$ent_id) {
451 $ent_id = 1;
452 }
453 $sid = sqimap_session_id();
454 // Don't kill the connection if the browser is over a dialup
455 // and it would take over 30 seconds to download it.
b7206e1d 456
457