Happy New Year
[squirrelmail.git] / functions / imap_messages.php
CommitLineData
59177427 1<?php
7350889b 2
35586184 3/**
258d61ed 4 * imap_messages.php
5 *
258d61ed 6 * This implements functions that manipulate messages
7 * NOTE: Quite a few functions in this file are obsolete
8 *
33aab559 9 * @copyright 1999-2024 The SquirrelMail Project Team
4b4abf93 10 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
258d61ed 11 * @version $Id$
12 * @package squirrelmail
13 * @subpackage imap
14 */
052e0c26 15
97f7ddf2 16
7c3e0802 17/**
8315c94c 18 * Copy a set of messages ($id) to another mailbox ($mailbox)
258d61ed 19 * @param int $imap_stream The resource ID for the IMAP socket
d462004f 20 * @param mixed $id Normally an array which is a list with message UIDs to be copied
21 * or a string range such as "1:*" or a simple integer
258d61ed 22 * @param string $mailbox The destination to copy to
4e6e5d2d 23 * @param bool $handle_errors Show error messages in case of a NO, BAD or BYE response
91c27aee 24 * @return bool If the copy completed without errors
258d61ed 25 */
4e6e5d2d 26function sqimap_msgs_list_copy($imap_stream, $id, $mailbox, $handle_errors = true) {
1c198ef7 27 $msgs_id = sqimap_message_list_squisher($id);
4e6e5d2d 28 $read = sqimap_run_command ($imap_stream, "COPY $msgs_id " . sqimap_encode_mailbox_name($mailbox), $handle_errors, $response, $message, TRUE);
324ac3c5 29 if ($response == 'OK') {
30 return true;
31 } else {
32 return false;
33 }
7c3e0802 34}
35
8315c94c 36
7c3e0802 37/**
8315c94c 38 * Move a set of messages ($id) to another mailbox. Deletes the originals.
258d61ed 39 * @param int $imap_stream The resource ID for the IMAP socket
40 * @param string $id The list of messages to move
41 * @param string $mailbox The destination to move to
4e6e5d2d 42 * @param bool $handle_errors Show error messages in case of a NO, BAD or BYE response
f171f05a 43 * @param string $source_mailbox (since 1.5.1) name of source mailbox. It is used to
821651ff 44 * validate that target mailbox != source mailbox.
4e6e5d2d 45 * @return bool If the move completed without errors
258d61ed 46 */
821651ff 47function sqimap_msgs_list_move($imap_stream, $id, $mailbox, $handle_errors = true, $source_mailbox = false) {
48 if ($source_mailbox!==false && $source_mailbox==$mailbox) {
49 return false;
50 }
4e6e5d2d 51 if (sqimap_msgs_list_copy ($imap_stream, $id, $mailbox, $handle_errors)) {
324ac3c5 52 return sqimap_toggle_flag($imap_stream, $id, '\\Deleted', true, true);
53 } else {
54 return false;
55 }
034fddf9 56}
57
58
d6c32258 59/**
258d61ed 60 * Deletes a message and move it to trash or expunge the mailbox
61 * @param resource imap connection
62 * @param string $mailbox mailbox, used for checking if it concerns the trash_folder
d462004f 63 * @param mixed $id Normally an array which is a list with message UIDs to be deleted
64 * or a string range such as "1:*" or a simple integer
83246804 65 * @param bool $bypass_trash (since 1.5.0) skip copy to trash
258d61ed 66 * @return array $aMessageList array with messages containing the new flags and UID @see parseFetch
83246804 67 * @since 1.4.0
258d61ed 68 */
8315c94c 69function sqimap_msgs_list_delete($imap_stream, $mailbox, $id, $bypass_trash=false) {
fbf11cec 70 // FIXME: Remove globals by introducing an associative array with properties as 4th argument as replacement for the $bypass_trash variable.
ec424753 71 global $move_to_trash, $trash_folder, $mark_as_read_upon_delete;
72 if ($mark_as_read_upon_delete)
73 sqimap_toggle_flag($imap_stream, $id, '\\Seen', true, true);
abafb676 74 if (($move_to_trash == true) && ($bypass_trash != true) &&
75 (sqimap_mailbox_exists($imap_stream, $trash_folder) && ($mailbox != $trash_folder)) ) {
a2aa472a 76 /**
77 * turn off internal error handling (fourth argument = false) and
78 * ignore copy to trash errors (allows to delete messages when overquota)
79 */
80 sqimap_msgs_list_copy ($imap_stream, $id, $trash_folder, false);
034fddf9 81 }
a2aa472a 82 return sqimap_toggle_flag($imap_stream, $id, '\\Deleted', true, true);
034fddf9 83}
84
85
258d61ed 86/**
87 * Set a flag on the provided uid list
88 * @param resource imap connection
694205bb 89 * @param mixed $id Normally an array which is a list with message UIDs to be flagged
d462004f 90 * or a string range such as "1:*" or a simple integer
258d61ed 91 * @param string $flag Flags to set/unset flags can be i.e.'\Seen', '\Answered', '\Seen \Answered'
92 * @param bool $set add (true) or remove (false) the provided flag
93 * @param bool $handle_errors Show error messages in case of a NO, BAD or BYE response
94 * @return array $aMessageList array with messages containing the new flags and UID @see parseFetch
95 */
034fddf9 96function sqimap_toggle_flag($imap_stream, $id, $flag, $set, $handle_errors) {
034fddf9 97 $msgs_id = sqimap_message_list_squisher($id);
98 $set_string = ($set ? '+' : '-');
f6382d6b 99
d462004f 100 /*
101 * We need to return the data in the same order as the caller supplied
102 * in $id, but IMAP servers are free to return responses in
103 * whatever order they wish... So we need to re-sort manually
104 */
694205bb 105 $aMessageList = array();
d462004f 106 if (is_array($id)) {
107 for ($i=0; $i<count($id); $i++) {
108 $aMessageList[$id[$i]] = array();
694205bb 109 }
f6382d6b 110 }
111
324ac3c5 112 $aResponse = sqimap_run_command_list($imap_stream, "STORE $msgs_id ".$set_string."FLAGS ($flag)", $handle_errors, $response, $message, TRUE);
f6382d6b 113
93f04c2c 114 // parse the fetch response
f6382d6b 115 $parseFetchResults=parseFetch($aResponse,$aMessageList);
116
f6382d6b 117 // some broken IMAP servers do not return UID elements on UID STORE
118 // if this is the case, then we need to do a UID FETCH
694205bb 119 if (!empty($parseFetchResults)
9002ae85 120 && !isset($parseFetchResults['UID'])) {
f6382d6b 121 $aResponse = sqimap_run_command_list($imap_stream, "FETCH $msgs_id (FLAGS)", $handle_errors, $response, $message, TRUE);
122 $parseFetchResults = parseFetch($aResponse,$aMessageList);
123 }
124
125 return ($parseFetchResults);
034fddf9 126}
127
8315c94c 128
48af4b64 129/**
258d61ed 130 * Sort the message list and crunch to be as small as possible
131 * (overflow could happen, so make it small if possible)
50d214a8 132 * @param array $aUid array with uid's
133 * @return string $s message set string
258d61ed 134 */
50d214a8 135function sqimap_message_list_squisher($aUid) {
136 if( !is_array( $aUid ) ) {
137 return $aUid;
97f7ddf2 138 }
50d214a8 139 sort($aUid, SORT_NUMERIC);
140
141 if (count($aUid)) {
142 $s = '';
143 for ($i=0,$iCnt=count($aUid);$i<$iCnt;++$i) {
144 $iStart = $aUid[$i];
145 $iEnd = $iStart;
146 while ($i<($iCnt-1) && $aUid[$i+1] == $iEnd +1) {
147 $iEnd = $aUid[$i+1];
148 ++$i;
149 }
150 if ($s) {
151 $s .= ',';
152 }
153 $s .= $iStart;
154 if ($iStart != $iEnd) {
155 $s .= ':' . $iEnd;
156 }
97f7ddf2 157 }
158 }
50d214a8 159 return $s;
3411d4ec 160}
97f7ddf2 161
8315c94c 162
48af4b64 163/**
8315c94c 164 * Retrieves an array with a sorted uid list. Sorting is done on the imap server
165 * @link http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-17.txt
166 * @param resource $imap_stream IMAP socket connection
167 * @param string $sSortField Field to sort on
168 * @param bool $reverse Reverse order search
169 * @return array $id sorted uid list
170 */
171function sqimap_get_sort_order($imap_stream, $sSortField, $reverse, $search='ALL') {
ce68b76b 172 global $default_charset;
2d34da11 173
ffb776c4 174 if ($sSortField) {
175 if ($reverse) {
176 $sSortField = 'REVERSE '.$sSortField;
177 }
324ac3c5 178 $query = "SORT ($sSortField) ".strtoupper($default_charset)." $search";
fbf11cec 179 // FIXME: sqimap_run_command() should return the parsed data accessible by $aDATA['SORT']
180 // use sqimap_run_command_list() in case of unsolicited responses. If we don't we could loose the SORT response.
f171f05a 181 $aData = sqimap_run_command_list ($imap_stream, $query, false, $response, $message, TRUE);
324ac3c5 182 /* fallback to default charset */
4ae9beb7 183 if ($response == 'NO') {
6283bb2e 184 if (strpos($message,'BADCHARSET') !== false ||
9aeac85e 185 strpos($message,'character') !== false) {
4ae9beb7 186 sqm_trigger_imap_error('SQM_IMAP_BADCHARSET',$query, $response, $message);
187 $query = "SORT ($sSortField) US-ASCII $search";
188 $aData = sqimap_run_command_list ($imap_stream, $query, true, $response, $message, TRUE);
189 } else {
190 sqm_trigger_imap_error('SQM_IMAP_ERROR',$query, $response, $message);
191 }
192 } else if ($response == 'BAD') {
193 sqm_trigger_imap_error('SQM_IMAP_NO_SORT',$query, $response, $message);
cdca177a 194 }
0fdc2fb6 195 }
324ac3c5 196
197 if ($response == 'OK') {
198 return parseUidList($aData,'SORT');
ffb776c4 199 } else {
324ac3c5 200 return false;
201 }
202}
203
258d61ed 204
205/**
8315c94c 206 * Parses a UID list returned on a SORT or SEARCH request
f171f05a 207 * @param array $aData imap response (retrieved from sqimap_run_command_list)
8315c94c 208 * @param string $sCommand issued imap command (SEARCH or SORT)
209 * @return array $aUid uid list
210 */
324ac3c5 211function parseUidList($aData,$sCommand) {
212 $aUid = array();
213 if (isset($aData) && count($aData)) {
214 for ($i=0,$iCnt=count($aData);$i<$iCnt;++$i) {
f171f05a 215 for ($j=0,$jCnt=count($aData[$i]);$j<$jCnt;++$j) {
216 if (preg_match("/^\* $sCommand (.+)$/", $aData[$i][$j], $aMatch)) {
a895042a 217 $aUid += explode(' ', trim($aMatch[1]));
f171f05a 218 }
324ac3c5 219 }
220 }
cdca177a 221 }
324ac3c5 222 return array_unique($aUid);
aa0da530 223}
2d34da11 224
26b22b20 225/**
258d61ed 226 * Retrieves an array with a sorted uid list. Sorting is done by SquirrelMail
227 *
228 * @param resource $imap_stream IMAP socket connection
229 * @param string $sSortField Field to sort on
230 * @param bool $reverse Reverse order search
231 * @param array $aUid limit the search to the provided array with uid's default sqimap_get_small_headers uses 1:*
232 * @return array $aUid sorted uid list
233 */
8315c94c 234function get_squirrel_sort($imap_stream, $sSortField, $reverse = false, $aUid = NULL) {
e0e30169 235 if ($sSortField != 'RFC822.SIZE' && $sSortField != 'INTERNALDATE') {
324ac3c5 236 $msgs = sqimap_get_small_header_list($imap_stream, $aUid,
e0e30169 237 array($sSortField), array());
ffb776c4 238 } else {
324ac3c5 239 $msgs = sqimap_get_small_header_list($imap_stream, $aUid,
e0e30169 240 array(), array($sSortField));
ffb776c4 241 }
d1c87b12 242
243 // sqimap_get_small_header (see above) returns fields in lower case,
244 // but the code below uses all upper case
c97c9b89 245 foreach ($msgs as $k => $v)
246 if (isset($msgs[$k][strtolower($sSortField)]))
247 $msgs[$k][strtoupper($sSortField)] = $msgs[$k][strtolower($sSortField)];
d1c87b12 248
c2e29558 249 $aUid = array();
76f29d49 250 $walk = false;
ffb776c4 251 switch ($sSortField) {
76f29d49 252 // natcasesort section
ffb776c4 253 case 'FROM':
ffb776c4 254 case 'TO':
76f29d49 255 case 'CC':
256 if(!$walk) {
9cfb0b2e 257 if (check_php_version(5, 3, 0))
258 $walk_function = function(&$v,&$k,$f) {
259 $v[$f] = (isset($v[$f])) ? $v[$f] : "";
260 $addr = reset(parseRFC822Address($v[$f],1));
261 $sPersonal = (isset($addr[SQM_ADDR_PERSONAL]) && $addr[SQM_ADDR_PERSONAL]) ?
262 $addr[SQM_ADDR_PERSONAL] : "";
263 $sEmail = ($addr[SQM_ADDR_HOST]) ?
264 $addr[SQM_ADDR_MAILBOX] . "@".$addr[SQM_ADDR_HOST] :
265 $addr[SQM_ADDR_HOST];
266 $v[$f] = ($sPersonal) ? decodeHeader($sPersonal, true, false):$sEmail;
267 };
268 else
269 $walk_function = create_function('&$v,&$k,$f',
270 '$v[$f] = (isset($v[$f])) ? $v[$f] : "";
271 $addr = reset(parseRFC822Address($v[$f],1));
272 $sPersonal = (isset($addr[SQM_ADDR_PERSONAL]) && $addr[SQM_ADDR_PERSONAL]) ?
273 $addr[SQM_ADDR_PERSONAL] : "";
274 $sEmail = ($addr[SQM_ADDR_HOST]) ?
275 $addr[SQM_ADDR_MAILBOX] . "@".$addr[SQM_ADDR_HOST] :
276 $addr[SQM_ADDR_HOST];
277 $v[$f] = ($sPersonal) ? decodeHeader($sPersonal, true, false):$sEmail;');
278 array_walk($msgs, $walk_function, $sSortField);
76f29d49 279 $walk = true;
cdca177a 280 }
76f29d49 281 // nobreak
ffb776c4 282 case 'SUBJECT':
76f29d49 283 if(!$walk) {
9cfb0b2e 284 if (check_php_version(5, 3, 0))
285 $walk_function = function(&$v,&$k,$f) {
286 $v[$f] = (isset($v[$f])) ? $v[$f] : "";
287 $v[$f] = strtolower(decodeHeader(trim($v[$f]), true, false));
288 $v[$f] = (preg_match("/^(?:(?:vedr|sv|re|aw|fw|fwd|\[\w\]):\s*)*\s*(.*)$/si", $v[$f], $matches)) ?
289 $matches[1] : $v[$f];
290 };
291 else
292 $walk_function = create_function('&$v,&$k,$f',
293 '$v[$f] = (isset($v[$f])) ? $v[$f] : "";
294 $v[$f] = strtolower(decodeHeader(trim($v[$f]), true, false));
295 $v[$f] = (preg_match("/^(?:(?:vedr|sv|re|aw|fw|fwd|\[\w\]):\s*)*\s*(.*)$/si", $v[$f], $matches)) ?
296 $matches[1] : $v[$f];');
297 array_walk($msgs, $walk_function, $sSortField);
76f29d49 298 $walk = true;
299 }
ffb776c4 300 foreach ($msgs as $item) {
324ac3c5 301 $aUid[$item['UID']] = $item[$sSortField];
ffb776c4 302 }
76f29d49 303 natcasesort($aUid);
304 $aUid = array_keys($aUid);
ffb776c4 305 if ($reverse) {
e432a21c 306 $aUid = array_reverse($aUid);
ffb776c4 307 }
308 break;
76f29d49 309 // \natcasesort section
310 // sort_numeric section
ffb776c4 311 case 'DATE':
76f29d49 312 case 'INTERNALDATE':
313 if(!$walk) {
9cfb0b2e 314 if (check_php_version(5, 3, 0))
315 $walk_function = function(&$v,$k,$f) {
316 $v[$f] = (isset($v[$f])) ? $v[$f] : "";
317 $v[$f] = getTimeStamp(explode(" ",$v[$f]));
318 };
319 else
320 $walk_function = create_function('&$v,$k,$f',
321 '$v[$f] = (isset($v[$f])) ? $v[$f] : "";
322 $v[$f] = getTimeStamp(explode(" ",$v[$f]));');
323 array_walk($msgs, $walk_function, $sSortField);
76f29d49 324 $walk = true;
ffb776c4 325 }
76f29d49 326 // nobreak;
ffb776c4 327 case 'RFC822.SIZE':
c2e29558 328 if(!$walk) {
329 // redefine $sSortField to maintain the same namespace between
598294a7 330 // server-side sorting and SquirrelMail sorting
c2e29558 331 $sSortField = 'SIZE';
332 }
ffb776c4 333 foreach ($msgs as $item) {
324ac3c5 334 $aUid[$item['UID']] = (isset($item[$sSortField])) ? $item[$sSortField] : 0;
ffb776c4 335 }
336 if ($reverse) {
76f29d49 337 arsort($aUid,SORT_NUMERIC);
ffb776c4 338 } else {
76f29d49 339 asort($aUid, SORT_NUMERIC);
ffb776c4 340 }
76f29d49 341 $aUid = array_keys($aUid);
ffb776c4 342 break;
76f29d49 343 // \sort_numeric section
ffb776c4 344 case 'UID':
76f29d49 345 $aUid = array_reverse($msgs);
ffb776c4 346 break;
6201339c 347 }
76f29d49 348 return $aUid;
cdca177a 349}
350
48af4b64 351/**
258d61ed 352 * Returns an array with each element as a string representing one
353 * message-thread as returned by the IMAP server.
9a864f82 354 * @param resource $imap_stream IMAP socket connection
355 * @param string $search optional search string
356 * @return array
258d61ed 357 * @link http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-13.txt
358 */
8315c94c 359function get_thread_sort($imap_stream, $search='ALL') {
9a864f82 360 global $sort_by_ref, $default_charset;
324ac3c5 361
7c612fdd 362 if ($sort_by_ref == 1) {
363 $sort_type = 'REFERENCES';
76f29d49 364 } else {
7c612fdd 365 $sort_type = 'ORDEREDSUBJECT';
366 }
324ac3c5 367 $query = "THREAD $sort_type ".strtoupper($default_charset)." $search";
368
4ae9beb7 369 // TODO use sqimap_run_command_list as we do in get_server_sort()
9a864f82 370 $sRead = sqimap_run_command ($imap_stream, $query, false, $response, $message, TRUE);
4ae9beb7 371
9a864f82 372 /* fallback to default charset */
4ae9beb7 373 if ($response == 'NO') {
6283bb2e 374 if (strpos($message,'BADCHARSET') !== false ||
9aeac85e 375 strpos($message,'character') !== false) {
4ae9beb7 376 sqm_trigger_imap_error('SQM_IMAP_BADCHARSET',$query, $response, $message);
377 $query = "THREAD $sort_type US-ASCII $search";
9a864f82 378 $sRead = sqimap_run_command ($imap_stream, $query, true, $response, $message, TRUE);
4ae9beb7 379 } else {
380 sqm_trigger_imap_error('SQM_IMAP_ERROR',$query, $response, $message);
381 }
382 } elseif ($response == 'BAD') {
383 sqm_trigger_imap_error('SQM_IMAP_NO_THREAD',$query, $response, $message);
324ac3c5 384 }
8ae7f0d1 385 $sThreadResponse = '';
9a864f82 386 if (isset($sRead[0])) {
387 for ($i=0,$iCnt=count($sRead);$i<$iCnt;++$i) {
388 if (preg_match("/^\* THREAD (.+)$/", $sRead[$i], $aMatch)) {
389 $sThreadResponse = trim($aMatch[1]);
76f29d49 390 break;
391 }
1c198ef7 392 }
7c612fdd 393 }
9a864f82 394 unset($sRead);
395
396 if ($response !== 'OK') {
397 return false;
474528eb 398 }
76f29d49 399
9a864f82 400 /* Example response
401 * S: * THREAD (2)(3 6 (4 23)(44 7 96))
402 * -- 2
76f29d49 403 *
9a864f82 404 * -- 3
405 * \-- 6
406 * |-- 4
407 * | \-- 23
408 * |
409 * \-- 44
410 * \-- 7
411 * \-- 96
76f29d49 412 */
9a864f82 413/*
414 * Notes for future work:
415 * indent_array should contain: indent_level, parent and flags,
416 * sibling nodes ..
417 * To achieve that we need to define the following flags:
418 * 0: hasnochildren
419 * 1: haschildren
420 * 2: is first
421 * 4: is last
422 * a node has sibling nodes if it's not the last node
423 * a node has no sibling nodes if it's the last node
424 * By using binary comparations we can store the flag in one var
425 *
426 * example:
427 * -1 par = 0, level = 0, flag = 1 + 2 + 4 = 7 (haschildren, isfirst, islast)
428 * \-2 par = 1, level = 1, flag = 0 + 2 = 2 (hasnochildren, isfirst)
429 * |-3 par = 1, level = 1, flag = 1 + 4 = 5 (haschildren, islast)
430 * \-4 par = 3, level = 2, flag = 1 + 2 + 4 = 7 (haschildren, isfirst, islast)
431 * \-5 par = 4, level = 3, flag = 0 + 2 + 4 = 6 (hasnochildren, isfirst, islast)
432 */
433
434 $j = 0;
435 $k = 0;
436 $l = 0;
437 $aUidThread = array();
438 $aIndent = array();
439 $aUidSubThread = array();
440 $aDepthStack = array();
441 $sUid = '';
442
443 if ($sThreadResponse) {
444 for ($i=0,$iCnt = strlen($sThreadResponse);$i<$iCnt;++$i) {
1710ad65 445 $cChar = $sThreadResponse[$i];
9a864f82 446 switch ($cChar) {
447 case '(': // new sub thread
3b8fe16c 448 // correction for a subthread of a thread with no parents in thread
449 if (!count($aUidSubThread) && $j > 0) {
450 --$l;
451 }
9a864f82 452 $aDepthStack[$j] = $l;
453 ++$j;
454 break;
455 case ')': // close sub thread
456 if($sUid !== '') {
457 $aUidSubThread[] = $sUid;
458 $aIndent[$sUid] = $j + $l - 1;
459 ++$l;
460 $sUid = '';
461 }
462 --$j;
463 if ($j === 0) {
464 // show message that starts the thread first.
465 $aUidSubThread = array_reverse($aUidSubThread);
466 // do not use array_merge because it's extremely slow and is causing timeouts
467 foreach ($aUidSubThread as $iUid) {
468 $aUidThread[] = $iUid;
469 }
470 $aUidSubThread = array();
471 $l = 0;
472 $aDepthStack = array();
473 } else {
474 $l = $aDepthStack[$j];
475 }
476 break;
477 case ' ': // new child
478 if ($sUid !== '') {
479 $aUidSubThread[] = $sUid;
480 $aIndent[$sUid] = $j + $l - 1;
481 ++$l;
482 $sUid = '';
483 }
484 break;
485 default: // part of UID
486 $sUid .= $cChar;
487 break;
76f29d49 488 }
ffb776c4 489 }
7c612fdd 490 }
9a864f82 491 unset($sThreadResponse);
492 // show newest threads first
493 $aUidThread = array_reverse($aUidThread);
494 return array($aUidThread,$aIndent);
7c612fdd 495}
496
034fddf9 497
7b07404c 498function elapsedTime($start) {
0fdc2fb6 499 $stop = gettimeofday();
500 $timepassed = 1000000 * ($stop['sec'] - $start['sec']) + $stop['usec'] - $start['usec'];
501 return $timepassed;
7b07404c 502}
7c612fdd 503
258d61ed 504/**
505 * Parses a string in an imap response. String starts with " or { which means it
506 * can handle double quoted strings and literal strings
507 *
508 * @param string $read imap response
509 * @param integer $i (reference) offset in string
510 * @return string $s parsed string without the double quotes or literal count
511 */
a18594b2 512function parseString($read,&$i) {
1710ad65 513 $char = $read[$i];
a18594b2 514 $s = '';
515 if ($char == '"') {
0fdc2fb6 516 $iPos = ++$i;
517 while (true) {
518 $iPos = strpos($read,'"',$iPos);
519 if (!$iPos) break;
1710ad65 520 if ($iPos && $read[$iPos -1] != '\\') {
8315c94c 521 $s = substr($read,$i,($iPos-$i));
522 $i = $iPos;
523 break;
524 }
525 $iPos++;
526 if ($iPos > strlen($read)) {
527 break;
528 }
0fdc2fb6 529 }
a18594b2 530 } else if ($char == '{') {
531 $lit_cnt = '';
532 ++$i;
533 $iPos = strpos($read,'}',$i);
534 if ($iPos) {
8315c94c 535 $lit_cnt = substr($read, $i, $iPos - $i);
536 $i += strlen($lit_cnt) + 3; /* skip } + \r + \n */
537 /* Now read the literal */
538 $s = ($lit_cnt ? substr($read,$i,$lit_cnt): '');
539 $i += $lit_cnt;
540 /* temp bugfix (SM 1.5 will have a working clean version)
541 too much work to implement that version right now */
542 --$i;
0fdc2fb6 543 } else { /* should never happen */
a18594b2 544 $i += 3; /* } + \r + \n */
545 $s = '';
0fdc2fb6 546 }
a18594b2 547 } else {
0fdc2fb6 548 return false;
a18594b2 549 }
550 ++$i;
551 return $s;
552}
553
8315c94c 554
258d61ed 555/**
556 * Parses a string containing an array from an imap response. String starts with ( and end with )
557 *
558 * @param string $read imap response
559 * @param integer $i (reference) offset in string
560 * @return array $a
561 */
a18594b2 562function parseArray($read,&$i) {
563 $i = strpos($read,'(',$i);
564 $i_pos = strpos($read,')',$i);
565 $s = substr($read,$i+1,$i_pos - $i -1);
566 $a = explode(' ',$s);
567 if ($i_pos) {
568 $i = $i_pos+1;
569 return $a;
570 } else {
571 return false;
572 }
573}
8315c94c 574
575
258d61ed 576/**
577 * Retrieves a list with headers, flags, size or internaldate from the imap server
4d7369b0 578 *
91c27aee 579 * WARNING: function is not portable between SquirrelMail 1.2.x, 1.4.x and 1.5.x.
4d7369b0 580 * Output format, third argument and $msg_list array format requirements differ.
581 * @param stream $imap_stream imap connection
582 * @param array $msg_list array with id's to create a msgs set from
583 * @param array $aHeaderFields (since 1.5.0) requested header fields
584 * @param array $aFetchItems (since 1.5.0) requested other fetch items like FLAGS, RFC822.SIZE
9a3d9100 585 * @return array $aMessages associative array with messages. Key is the UID, value is an associative array
4d7369b0 586 * @since 1.1.3
258d61ed 587 */
8315c94c 588function sqimap_get_small_header_list($imap_stream, $msg_list,
91c27aee 589 $aHeaderFields = array('Date', 'To', 'Cc', 'From', 'Subject', 'X-Priority', 'Content-Type'),
8cc8ec79 590 $aFetchItems = array('FLAGS', 'RFC822.SIZE', 'INTERNALDATE')) {
ffb776c4 591
f09bea28 592 global $extra_small_header_fields;
593 if (!empty($extra_small_header_fields))
594 $aHeaderFields = array_merge($aHeaderFields, $extra_small_header_fields);
595
324ac3c5 596 $aMessageList = array();
ffb776c4 597
91c27aee 598 /**
599 * Catch other priority headers as well
600 */
601 if (in_array('X-Priority',$aHeaderFields,true)) {
602 $aHeaderFields[] = 'Importance';
603 $aHeaderFields[] = 'Priority';
604 }
605
c075fcfe 606 $bUidFetch = ! in_array('UID', $aFetchItems, true);
8cc8ec79 607
97f7ddf2 608 /* Get the small headers for each message in $msg_list */
258d61ed 609 if ($msg_list !== NULL) {
a18594b2 610 $msgs_str = sqimap_message_list_squisher($msg_list);
ffb776c4 611 /*
612 * We need to return the data in the same order as the caller supplied
613 * in $msg_list, but IMAP servers are free to return responses in
614 * whatever order they wish... So we need to re-sort manually
615 */
8cc8ec79 616 if ($bUidFetch) {
59b0de95 617 // 1.4.x had a problem where $msg_list wasn't guaranteed to be an array,
618 // but the following doesn't seem to be necessary in 1.5.x
619 $msg_count = is_array($msg_list) ? sizeof($msg_list) : 1;
620 for ($i = 0; $i < $msg_count; $i++) {
d462004f 621 $aMessageList[$msg_list[$i]] = array();
8cc8ec79 622 }
ffb776c4 623 }
1c198ef7 624 } else {
a18594b2 625 $msgs_str = '1:*';
626 }
ffb776c4 627
3411d4ec 628 /*
ffb776c4 629 * Create the query
630 */
cdca177a 631
ffb776c4 632 $sFetchItems = '';
633 $query = "FETCH $msgs_str (";
634 if (count($aFetchItems)) {
635 $sFetchItems = implode(' ',$aFetchItems);
636 }
637 if (count($aHeaderFields)) {
638 $sHeaderFields = implode(' ',$aHeaderFields);
639 $sFetchItems .= ' BODY.PEEK[HEADER.FIELDS ('.$sHeaderFields.')]';
7b07404c 640 }
ffb776c4 641 $query .= trim($sFetchItems) . ')';
324ac3c5 642 $aResponse = sqimap_run_command_list ($imap_stream, $query, true, $response, $message, $bUidFetch);
643 $aMessages = parseFetch($aResponse,$aMessageList);
644 array_reverse($aMessages);
645 return $aMessages;
646}
8cc8ec79 647
8315c94c 648
258d61ed 649/**
650 * Parses a fetch response, currently it can hande FLAGS, HEADERS, RFC822.SIZE, INTERNALDATE and UID
651 * @param array $aResponse Imap response
d462004f 652 * @param array $aMessageList Placeholder array for results. The keys of this
653 * placeholder array should be the UID so we can
654 * reconstruct the order (OPTIONAL; this array will
655 * be built for the return value fresh if not given)
258d61ed 656 * @return array $aMessageList associative array with messages. Key is the UID, value is an associative array
657 * @author Marc Groot Koerkamp
658 */
3b8fe16c 659function parseFetch(&$aResponse,$aMessageList = array()) {
91c27aee 660 for ($j=0,$iCnt=count($aResponse);$j<$iCnt;++$j) {
661 $aMsg = array();
a18594b2 662
91c27aee 663 $read = implode('',$aResponse[$j]);
664 // free up memmory
665 unset($aResponse[$j]); /* unset does not reindex the array. the for loop is safe */
1c198ef7 666 /*
91c27aee 667 * #id<space>FETCH<space>(
668 */
1c198ef7 669
a18594b2 670 /* extract the message id */
91c27aee 671 $i_space = strpos($read,' ',2);/* position 2ed <space> */
672 $id = substr($read,2/* skip "*<space>" */,$i_space -2);
673 $aMsg['ID'] = $id;
a18594b2 674 $fetch = substr($read,$i_space+1,5);
675 if (!is_numeric($id) && $fetch !== 'FETCH') {
3047e291 676 $aMsg['ERROR'] = $read; // sm_encode_html_special_chars should be done just before display. this is backend code
8cc8ec79 677 break;
a18594b2 678 }
679 $i = strpos($read,'(',$i_space+5);
680 $read = substr($read,$i+1);
681 $i_len = strlen($read);
682 $i = 0;
683 while ($i < $i_len && $i !== false) {
684 /* get argument */
685 $read = trim(substr($read,$i));
686 $i_len = strlen($read);
687 $i = strpos($read,' ');
688 $arg = substr($read,0,$i);
689 ++$i;
91c27aee 690 /*
691 * use allcaps for imap items and lowcaps for headers as key for the $aMsg array
692 */
a18594b2 693 switch ($arg)
694 {
695 case 'UID':
696 $i_pos = strpos($read,' ',$i);
697 if (!$i_pos) {
698 $i_pos = strpos($read,')',$i);
cdca177a 699 }
a18594b2 700 if ($i_pos) {
701 $unique_id = substr($read,$i,$i_pos-$i);
702 $i = $i_pos+1;
703 } else {
704 break 3;
cdca177a 705 }
a18594b2 706 break;
707 case 'FLAGS':
708 $flags = parseArray($read,$i);
709 if (!$flags) break 3;
ffb776c4 710 $aFlags = array();
a18594b2 711 foreach ($flags as $flag) {
712 $flag = strtolower($flag);
ffb776c4 713 $aFlags[$flag] = true;
cdca177a 714 }
91c27aee 715 $aMsg['FLAGS'] = $aFlags;
a18594b2 716 break;
717 case 'RFC822.SIZE':
718 $i_pos = strpos($read,' ',$i);
719 if (!$i_pos) {
720 $i_pos = strpos($read,')',$i);
cdca177a 721 }
a18594b2 722 if ($i_pos) {
91c27aee 723 $aMsg['SIZE'] = substr($read,$i,$i_pos-$i);
a18594b2 724 $i = $i_pos+1;
725 } else {
726 break 3;
727 }
8cc8ec79 728 break;
729 case 'ENVELOPE':
91c27aee 730 // sqimap_parse_address($read,$i,$aMsg);
731 break; // to be implemented, moving imap code out of the Message class
8cc8ec79 732 case 'BODYSTRUCTURE':
91c27aee 733 break; // to be implemented, moving imap code out of the Message class
a18594b2 734 case 'INTERNALDATE':
8af247e6 735 $aMsg['INTERNALDATE'] = trim(preg_replace('/\s+/', ' ',parseString($read,$i)));
a18594b2 736 break;
737 case 'BODY.PEEK[HEADER.FIELDS':
738 case 'BODY[HEADER.FIELDS':
91c27aee 739 $i = strpos($read,'{',$i); // header is always returned as literal because it contain \n characters
a18594b2 740 $header = parseString($read,$i);
92a52cda 741 if ($header === false) break 2;
2a9b0fad 742 /* First we replace all \r\n by \n, and unfold the header */
743 $hdr = trim(str_replace(array("\r\n", "\n\t", "\n "),array("\n", ' ', ' '), $header));
91c27aee 744 /* Now we can make a new header array with
745 each element representing a headerline */
746 $aHdr = explode("\n" , $hdr);
2714d4ff 747 $aReceived = array();
91c27aee 748 foreach ($aHdr as $line) {
a18594b2 749 $pos = strpos($line, ':');
750 if ($pos > 0) {
751 $field = strtolower(substr($line, 0, $pos));
752 if (!strstr($field,' ')) { /* valid field */
753 $value = trim(substr($line, $pos+1));
91c27aee 754 switch($field) {
755 case 'date':
8af247e6 756 $aMsg['date'] = trim(preg_replace('/\s+/', ' ', $value));
91c27aee 757 break;
1710ad65 758 case 'x-priority': $aMsg['x-priority'] = ($value) ? (int) $value[0] : 3; break;
91c27aee 759 case 'priority':
760 case 'importance':
8b08e46d 761 // duplicate code with Rfc822Header.cls:parsePriority()
91c27aee 762 if (!isset($aMsg['x-priority'])) {
8b08e46d 763 $aPrio = preg_split('/\s/',trim($value));
ba17b6c7 764 $sPrio = strtolower(array_shift($aPrio));
765 if (is_numeric($sPrio)) {
766 $iPrio = (int) $sPrio;
767 } elseif ( $sPrio == 'non-urgent' || $sPrio == 'low' ) {
aa04b27d 768 $iPrio = 5;
ba17b6c7 769 } elseif ( $sPrio == 'urgent' || $sPrio == 'high' ) {
770 $iPrio = 1;
91c27aee 771 } else {
772 // default is normal priority
ba17b6c7 773 $iPrio = 3;
91c27aee 774 }
ba17b6c7 775 $aMsg['x-priority'] = $iPrio;
91c27aee 776 }
777 break;
778 case 'content-type':
779 $type = $value;
780 if ($pos = strpos($type, ";")) {
781 $type = substr($type, 0, $pos);
782 }
783 $type = explode("/", $type);
784 if(!is_array($type) || count($type) < 2) {
785 $aMsg['content-type'] = array('text','plain');
786 } else {
787 $aMsg['content-type'] = array(strtolower($type[0]),strtolower($type[1]));
788 }
789 break;
790 case 'received':
791 $aMsg['received'][] = $value;
792 break;
793 default:
794 $aMsg[$field] = $value;
795 break;
a18594b2 796 }
cdca177a 797 }
798 }
799 }
a18594b2 800 break;
801 default:
802 ++$i;
803 break;
cdca177a 804 }
cdca177a 805 }
628dba17 806 if (!empty($unique_id)) {
807 $msgi = "$unique_id";
808 $aMsg['UID'] = $unique_id;
809 } else {
d462004f 810//FIXME: under what circumstances does this happen? We can't use an empty string as an array index in the line just below, so we need to use something else here
628dba17 811 $msgi = '';
812 }
813 $aMessageList[$msgi] = $aMsg;
3b8fe16c 814 $aResponse[$j] = NULL;
97f7ddf2 815 }
324ac3c5 816 return $aMessageList;
97f7ddf2 817}
818
258d61ed 819/**
820 * Work in process
821 * @private
822 * @author Marc Groot Koerkamp
823 */
8cc8ec79 824function sqimap_parse_envelope($read, &$i, &$msg) {
825 $arg_no = 0;
826 $arg_a = array();
827 ++$i;
1710ad65 828 for ($cnt = strlen($read); ($i < $cnt) && ($read[$i] != ')'); ++$i) {
829 $char = strtoupper($read[$i]);
8cc8ec79 830 switch ($char) {
831 case '{':
832 case '"':
833 $arg_a[] = parseString($read,$i);
834 ++$arg_no;
835 break;
836 case 'N':
837 /* probably NIL argument */
838 if (strtoupper(substr($read, $i, 3)) == 'NIL') {
839 $arg_a[] = '';
840 ++$arg_no;
841 $i += 2;
842 }
843 break;
844 case '(':
845 /* Address structure (with group support)
846 * Note: Group support is useless on SMTP connections
847 * because the protocol doesn't support it
848 */
849 $addr_a = array();
850 $group = '';
851 $a=0;
1710ad65 852 for (; $i < $cnt && $read[$i] != ')'; ++$i) {
853 if ($read[$i] == '(') {
8cc8ec79 854 $addr = sqimap_parse_address($read, $i);
855 if (($addr[3] == '') && ($addr[2] != '')) {
856 /* start of group */
857 $group = $addr[2];
858 $group_addr = $addr;
859 $j = $a;
860 } else if ($group && ($addr[3] == '') && ($addr[2] == '')) {
861 /* end group */
862 if ($a == ($j+1)) { /* no group members */
863 $group_addr[4] = $group;
864 $group_addr[2] = '';
865 $group_addr[0] = "$group: Undisclosed recipients;";
866 $addr_a[] = $group_addr;
867 $group ='';
868 }
869 } else {
870 $addr[4] = $group;
871 $addr_a[] = $addr;
872 }
873 ++$a;
874 }
875 }
876 $arg_a[] = $addr_a;
877 break;
878 default: break;
879 }
880 }
881
882 if (count($arg_a) > 9) {
883 $d = strtr($arg_a[0], array(' ' => ' '));
884 $d = explode(' ', $d);
cf92500b 885 if (!$arg_a[1]) $arg_a[1] = '';
8cc8ec79 886 $msg['DATE'] = $d; /* argument 1: date */
887 $msg['SUBJECT'] = $arg_a[1]; /* argument 2: subject */
888 $msg['FROM'] = is_array($arg_a[2]) ? $arg_a[2][0] : ''; /* argument 3: from */
889 $msg['SENDER'] = is_array($arg_a[3]) ? $arg_a[3][0] : ''; /* argument 4: sender */
890 $msg['REPLY-TO'] = is_array($arg_a[4]) ? $arg_a[4][0] : ''; /* argument 5: reply-to */
891 $msg['TO'] = $arg_a[5]; /* argument 6: to */
892 $msg['CC'] = $arg_a[6]; /* argument 7: cc */
893 $msg['BCC'] = $arg_a[7]; /* argument 8: bcc */
894 $msg['IN-REPLY-TO'] = $arg_a[8]; /* argument 9: in-reply-to */
895 $msg['MESSAGE-ID'] = $arg_a[9]; /* argument 10: message-id */
896 }
897}
898
8315c94c 899
258d61ed 900/**
901 * Work in process
902 * @private
903 * @author Marc Groot Koerkamp
904 */
8cc8ec79 905function sqimap_parse_address($read, &$i) {
906 $arg_a = array();
1710ad65 907 for (; $read[$i] != ')'; ++$i) {
908 $char = strtoupper($read[$i]);
8cc8ec79 909 switch ($char) {
910 case '{':
911 case '"': $arg_a[] = parseString($read,$i); break;
912 case 'n':
913 case 'N':
914 if (strtoupper(substr($read, $i, 3)) == 'NIL') {
915 $arg_a[] = '';
916 $i += 2;
917 }
918 break;
919 default: break;
920 }
921 }
922
923 if (count($arg_a) == 4) {
924 return $arg_a;
925
926// $adr = new AddressStructure();
927// $adr->personal = $arg_a[0];
928// $adr->adl = $arg_a[1];
929// $adr->mailbox = $arg_a[2];
930// $adr->host = $arg_a[3];
931 } else {
932 $adr = '';
933 }
934 return $adr;
935}
936
8315c94c 937
48af4b64 938/**
258d61ed 939 * Returns a message array with all the information about a message.
940 * See the documentation folder for more information about this array.
941 *
942 * @param resource $imap_stream imap connection
943 * @param integer $id uid of the message
944 * @param string $mailbox used for error handling, can be removed because we should return an error code and generate the message elsewhere
48d015b4 945 * @param int $hide Indicates whether or not to hide any errors: 0 = don't hide, 1 = hide (just exit), 2 = hide (return FALSE), 3 = hide (return error string) (OPTIONAL; default don't hide)
946 * @return mixed Message object or FALSE/error string if error occurred and $hide is set to 2/3
258d61ed 947 */
1c9425d1 948function sqimap_get_message($imap_stream, $id, $mailbox, $hide=0) {
461eda6c 949 // typecast to int to prohibit 1:* msgs sets
51bbe8fa 950 // Update: $id should always be sanitized into a BIGINT so this
951 // is being removed; leaving this code here in case something goes
952 // wrong, however
953 //$id = (int) $id;
2d34da11 954 $flags = array();
8315c94c 955 $read = sqimap_run_command($imap_stream, "FETCH $id (FLAGS BODYSTRUCTURE)", true, $response, $message, TRUE);
114f2a24 956 if ($read) {
b69a13a4 957 if (preg_match('/.+FLAGS\s\((.*)\)\s/AUi',$read[0],$regs)) {
958 if (trim($regs[1])) {
75cd948c 959 $flags = preg_split('/ /', $regs[1],-1,PREG_SPLIT_NO_EMPTY);
b69a13a4 960 }
961 }
114f2a24 962 } else {
1c9425d1 963
964 if ($hide == 1) exit;
965 if ($hide == 2) return FALSE;
966
b69a13a4 967 /* the message was not found, maybe the mailbox was modified? */
ce8c6f42 968 global $sort, $startMessage;
b69a13a4 969
48d015b4 970 $errmessage = _("The server couldn't find the message you requested.");
971
972 if ($hide == 3) return $errmessage;
973
974 $errmessage .= '<p>'._("Most probably your message list was out of date and the message has been moved away or deleted (perhaps by another program accessing the same mailbox).");
975
b69a13a4 976 /* this will include a link back to the message list */
ce8c6f42 977 error_message($errmessage, $mailbox, $sort, (int) $startMessage);
b69a13a4 978 exit;
1c198ef7 979 }
2d34da11 980 $bodystructure = implode('',$read);
981 $msg = mime_structure($bodystructure,$flags);
8315c94c 982 $read = sqimap_run_command($imap_stream, "FETCH $id BODY[HEADER]", true, $response, $message, TRUE);
19d470aa 983 $rfc822_header = new Rfc822Header();
767ace1f 984 $rfc822_header->parseHeader($read);
985 $msg->rfc822_header = $rfc822_header;
a4f7d027 986
987 parse_message_entities($msg, $id, $imap_stream);
2d34da11 988 return $msg;
a4f7d027 989 }
990
991
992/**
993 * Recursively parse embedded messages (if any) in the given
994 * message, building correct rfc822 headers for each one
995 *
996 * @param object $msg The message object to scan for attached messages
997 * NOTE: this is passed by reference! Changes made
998 * within will affect the caller's copy of $msg!
999 * @param int $id The top-level message UID on the IMAP server, even
1000 * if the $msg being passed in is only an attached entity
1001 * thereof.
1002 * @param resource $imap_stream A live connection to the IMAP server.
1003 *
1004 * @return void
1005 *
1006 * @since 1.5.2
1007 *
1008 */
1009function parse_message_entities(&$msg, $id, $imap_stream) {
a4f7d027 1010 if (!empty($msg->entities)) foreach ($msg->entities as $i => $entity) {
e49ea4ad 1011 if (is_object($entity) && strtolower(get_class($entity)) == 'message') {
a4f7d027 1012 if (!empty($entity->rfc822_header)) {
0331a925 1013 $read = sqimap_run_command($imap_stream, "FETCH $id BODY[". $entity->entity_id .".HEADER]", true, $response, $message, TRUE);
a4f7d027 1014 $rfc822_header = new Rfc822Header();
1015 $rfc822_header->parseHeader($read);
1016 $msg->entities[$i]->rfc822_header = $rfc822_header;
1017 }
1018 parse_message_entities($msg->entities[$i], $id, $imap_stream);
1019 }
1020 }
97f7ddf2 1021}