Unfound mbox is now handled as if it was empty
[banana.git] / banana / mbox.inc.php
CommitLineData
7027794f 1<?php
2/********************************************************************************
3* banana/protocoleinterface.inc.php : interface for box access
4* ------------------------
5*
6* This file is part of the banana distribution
7* Copyright: See COPYING files that comes with this distribution
8********************************************************************************/
9
10require_once dirname(__FILE__) . '/banana.inc.php';
11require_once dirname(__FILE__) . '/protocoleinterface.inc.php';
12require_once dirname(__FILE__) . '/message.inc.php';
13
14class BananaMBox implements BananaProtocoleInterface
15{
7027794f 16 private $file = null;
17 private $filesize = null;
18 private $current_id = null;
19 private $at_beginning = false;
20 private $file_cache = null;
21
22 private $_lasterrno = 0;
23 private $_lasterror = null;
24
25 private $count = null;
26 private $new_messages = null;
27 private $messages = null;
28
29 /** Build a protocole handler plugged on the given box
30 */
0e25d15d 31 public function __construct()
7027794f 32 {
c28d3016 33 $filename = $this->getFileName();
7027794f 34 if (is_null($filename)) {
35 return;
36 }
7027794f 37 $this->file = @fopen($filename, 'r');
38 if (!$this->file) {
7027794f 39 $this->file = null;
c28d3016 40 $this->filesize = 0;
41 } else {
42 $this->filesize = filesize($filename);
7027794f 43 }
44 $this->current_id = 0;
45 $this->at_beginning = true;
46 }
47
48 /** Close the file
49 */
50 public function __destruct()
51 {
52 if ($this->file) {
53 fclose($this->file);
54 }
55 }
56
57 /** Indicate if the Protocole handler has been succesfully built
58 */
59 public function isValid()
60 {
c28d3016 61 return true;
62 //!Banana::$group || $this->file;
7027794f 63 }
64
598a1c53 65 /** Indicate last error n°
7027794f 66 */
67 public function lastErrNo()
68 {
69 return $this->_lasterrno;;
70 }
71
72 /** Indicate last error text
73 */
74 public function lastError()
75 {
76 return $this->_lasterror;
77 }
78
79 /** Return the description of the current box
80 */
81 public function getDescription()
82 {
83 return null;
84 }
85
86 /** Return the list of the boxes
87 * @param mode Kind of boxes to list
88 * @param since date of last check (for new boxes and new messages)
89 * @param withstats Indicated whether msgnum and unread must be set in the result
90 * @return Array(boxname => array(desc => boxdescripton, msgnum => number of message, unread =>number of unread messages)
91 */
92 public function getBoxList($mode = Banana::BOXES_ALL, $since = 0, $withstats = false)
93 {
0e25d15d 94 return array(Banana::$group => array('desc' => '', 'msgnum' => 0, 'unread' => 0));
7027794f 95 }
96
97 /** Return a message
98 * @param id Id of the emssage (can be either an Message-id or a message index)
7027794f 99 * @return A BananaMessage or null if the given id can't be retreived
100 */
7a5823f9 101 public function &getMessage($id)
7027794f 102 {
7d3f4749 103 $message = null;
c28d3016 104 if (is_null($this->file)) {
105 return $message;
106 }
7a5823f9 107 if (!is_numeric($id)) {
108 if (!Banana::$spool) {
7d3f4749 109 return $message;
7027794f 110 }
111 $id = Banana::$spool->ids[$id];
112 }
f06f42dc 113 $messages = $this->readMessages(array($id));
114 if (!empty($messages)) {
115 $message = new BananaMessage($messages[$id]['message']);
7027794f 116 }
3c3a3ce3 117 return $message;
7027794f 118 }
119
7a5823f9 120 /** Return the sources of the given message
121 */
122 public function getMessageSource($id)
123 {
7d3f4749 124 $message = null;
c28d3016 125 if (is_null($this->file)) {
126 return $message;
127 }
7a5823f9 128 if (!is_numeric($id)) {
129 if (!Banana::$spool) {
7d3f4749 130 return $message;
7a5823f9 131 }
132 $id = Banana::$spool->ids[$id];
7d3f4749 133 }
7a5823f9 134 $message = $this->readMessages(array($id));
7d3f4749 135 return implode("\n", $message[$id]['message']);
7a5823f9 136 }
137
138 /** Compute the number of messages of the box
139 */
7027794f 140 private function getCount()
141 {
142 $this->count = count(Banana::$spool->overview);
143 $max = @max(array_keys(Banana::$spool->overview));
144 if ($max && Banana::$spool->overview[$max]->storage['next'] == $this->filesize) {
145 $this->new_messages = 0;
146 } else {
147 $this->new_messages = $this->countMessages($this->count);
148 $this->count += $this->new_messages;
149 }
150 }
151
152 /** Return the indexes of the messages presents in the Box
153 * @return Array(number of messages, MSGNUM of the first message, MSGNUM of the last message)
154 */
155 public function getIndexes()
156 {
c28d3016 157 if (is_null($this->file)) {
158 return array(0, 0, 0);
159 }
7027794f 160 if (is_null($this->count)) {
161 $this->getCount();
162 }
163 return array($this->count, 0, $this->count - 1);
164 }
165
166 /** Return the message headers (in BananaMessage) for messages from firstid to lastid
167 * @return Array(id => array(headername => headervalue))
168 */
169 public function &getMessageHeaders($firstid, $lastid, array $msg_headers = array())
170 {
171 $msg_headers = array_map('strtolower', $msg_headers);
172 $messages =& $this->readMessages(range($firstid, $lastid), true);
173 $msg_headers = array_map('strtolower', $msg_headers);
174 $headers = array();
c28d3016 175 if (is_null($this->file)) {
176 return $headers;
177 }
7027794f 178 foreach ($msg_headers as $header) {
179 foreach ($messages as $id=>&$message) {
180 if (!isset($headers[$id])) {
181 $headers[$id] = array('beginning' => $message['beginning'], 'end' => $message['end']);
182 }
183 if ($header == 'date') {
7a5823f9 184 $headers[$id][$header] = @strtotime($message['message'][$header]);
7027794f 185 } else {
7a5823f9 186 $headers[$id][$header] = @$message['message'][$header];
7027794f 187 }
188 }
189 }
190 unset($this->messages);
191 unset($messages);
192 return $headers;
193 }
194
195 /** Add storage data in spool overview
196 */
197 public function updateSpool(array &$messages)
198 {
199 foreach ($messages as $id=>&$data) {
200 if (isset(Banana::$spool->overview[$id])) {
201 Banana::$spool->overview[$id]->storage['offset'] = $data['beginning'];
202 Banana::$spool->overview[$id]->storage['next'] = $data['end'];
203 }
204 }
205 }
206
207 /** Return the indexes of the new messages since the give date
208 * @return Array(MSGNUM of new messages)
209 */
210 public function getNewIndexes($since)
211 {
c28d3016 212 if (is_null($this->file)) {
213 return array();
214 }
7027794f 215 if (is_null($this->new_messages)) {
216 $this->getCount();
217 }
218 return range($this->count - $this->new_messages, $this->count - 1);
219 }
220
221 /** Return wether or not the protocole can be used to add new messages
222 */
223 public function canSend()
224 {
225 return true;
226 }
227
228 /** Return false because we can't cancel a mail
229 */
230 public function canCancel()
231 {
232 return false;
233 }
234
235 /** Return the list of requested headers
236 * @return Array('header1', 'header2', ...) with the key 'dest' for the destination header
237 * and 'reply' for the reply header, eg:
238 * * for a mail: Array('From', 'Subject', 'dest' => 'To', 'Cc', 'Bcc', 'reply' => 'Reply-To')
239 * * for a post: Array('From', 'Subject', 'dest' => 'Newsgroups', 'reply' => 'Followup-To')
240 */
241 public function requestedHeaders()
242 {
243 return Array('From', 'Subject', 'dest' => 'To', 'Cc', 'Bcc', 'reply' => 'Reply-To');
244 }
245
246 /** Send a message
247 * @return true if it was successfull
248 */
249 public function send(BananaMessage &$message)
250 {
e1debf92 251 $headers = $message->getHeaders();
252 $to = $headers['To'];
253 $subject = $headers['Subject'];
254 unset($headers['To']);
255 unset($headers['Subject']);
256 $hdrs = '';
257 foreach ($headers as $key=>$value) {
258 if (!empty($value)) {
259 $hdrs .= "$key: $value\r\n";
260 }
261 }
262 $body = $message->get(false);
263 return mail($to, $subject, $body, $hdrs);
7027794f 264 }
265
266 /** Cancel a message
267 * @return true if it was successfull
268 */
269 public function cancel(BananaMessage &$message)
270 {
271 return false;
272 }
273
274 /** Return the protocole name
275 */
276 public function name()
277 {
278 return 'MBOX';
279 }
280
e9360b11 281 /** Return the spool filename
282 */
283 public function filename()
284 {
285 @list($mail, $domain) = explode('@', Banana::$group);
286 $file = "";
287 if (isset($domain)) {
288 $file = $domain . '_';
289 }
290 return $file . $mail;
291 }
292
7027794f 293#######
294# Filesystem functions
295#######
296
c28d3016 297 protected function getFileName()
7027794f 298 {
c28d3016 299 if (is_null(Banana::$group)) {
7027794f 300 return null;
301 }
c28d3016 302 @list($mail, $domain) = explode('@', Banana::$group);
e9360b11 303 return Banana::$mbox_path . '/' . $mail;
7027794f 304 }
305
306#######
307# MBox parser
308#######
309
310 /** Go to the given message
311 */
312 private function goTo($id)
313 {
314 if ($this->current_id == $id && $this->at_beginning) {
315 return true;
316 }
317 if ($id == 0) {
318 fseek($this->file, 0);
319 $this->current_id = 0;
320 $this->at_beginning = true;
321 return true;
322 } elseif (isset(Banana::$spool->overview[$id]) || isset($this->messages[$id])) {
323 if (isset(Banana::$spool->overview[$id])) {
324 $pos = Banana::$spool->overview[$id]->storage['offset'];
325 } else {
326 $pos = $this->messages[$id]['beginning'];
327 }
328 if (fseek($this->file, $pos) == 0) {
329 $this->current_id = $id;
330 $this->at_beginning = true;
331 return true;
332 } else {
333 $this->current_id = null;
334 $this->_lasterrno = 2;
335 $this->_lasterror = _b_('Can\'t find message ') . $id;
336 return false;
337 }
338 } else {
339 $max = @max(array_keys(Banana::$spool->overview));
340 if (is_null($max)) {
341 $max = 0;
342 }
343 if ($id <= $max && $max != 0) {
344 $this->current_id = null;
345 $this->_lasterrno = 3;
346 $this->_lasterror = _b_('Invalid message index ') . $id;
347 return false;
348 }
349 if (!$this->goTo($max)) {
350 return false;
351 }
352 if (feof($this->file)) {
353 $this->current_id = null;
354 $this->_lasterrno = 4;
355 $this->_lasterror = _b_('Requested index does not exists or file has been truncated');
356 return false;
357 }
358 while ($this->readCurrentMessage(true) && $this->current_id < $id);
359 if ($this->current_id == $id) {
360 return true;
361 }
362 $this->current_id = null;
363 $this->_lasterrno = 5;
364 $this->_lasterror = _b_('Requested index does not exists or file has been truncated');
365 return false;
366 }
367 }
368
369 private function countMessages($from = 0)
370 {
371 $this->messages =& $this->readMessages(array($from), true, true);
372 return count($this->messages);
373 }
374
375 /** Read the current message (identified by current_id)
376 * @param needFrom_ BOOLEAN is true if the first line *must* be a From_ line
377 * @param alignNext BOOLEAN is true if the buffer must be aligned at the beginning of the next From_ line
378 * @return message sources (without storage data)
379 */
380 private function &readCurrentMessage($stripBody = false, $needFrom_ = true, $alignNext = true)
381 {
382 $file_cache =& $this->file_cache;
383 if ($file_cache && $file_cache != ftell($this->file)) {
384 $file_cache = null;
385 }
386 $msg = array();
387 $canFrom_ = false;
388 $inBody = false;
389 while(!feof($this->file)) {
390 // Process file cache
391 if ($file_cache) { // this is a From_ line
392 $needFrom_ = false;
393 $this->at_beginning = false;
394 $file_cache = null;
395 continue;
396 }
397
398 // Read a line
399 $line = rtrim(fgets($this->file), "\r\n");
400
401 // Process From_ line
402 if ($needFrom_ || !$msg || $canFrom_) {
403 if (substr($line, 0, 5) == 'From ') { // this is a From_ line
404 if ($needFrom_) {
405 $needFrom = false;
406 } elseif (!$msg) {
407 continue;
408 } else {
409 $this->current_id++; // we are finally in the next message
410 if ($alignNext) { // align the file pointer at the beginning of the new message
411 $this->at_beginning = true;
412 $file_cache = ftell($this->file);
413 }
414 break;
415 }
416 } elseif ($needFrom_) {
417 return $msg;
418 }
419 }
420
421 // Process non-From_ lines
422 if (substr($line, 0, 6) == '>From ') { // remove inline From_ quotation
423 $line = substr($line, 1);
424 }
425 if (!$stripBody || !$inBody) {
426 $msg[] = $line; // add the line to the message source
427 }
428 $canFrom_ = empty($line); // check if next line can be a From_ line
429 if ($canFrom_ && !$inBody && $stripBody) {
430 $inBody = true;
431 }
432 $this->at_beginning = false;
433 }
434 if (!feof($this->file) && !$canFrom_) {
435 $msg = array();
436 }
437 return $msg;
438 }
439
440 /** Read message with the given ids
441 * @param ids ARRAY of ids to look for
442 * @param strip BOOLEAN if true, only headers are retrieved
443 * @param from BOOLEAN if true, process all messages from max(ids) to the end of the mbox
444 * @return Array(Array('message' => message sources (or parsed message headers if $strip is true),
445 * 'beginning' => offset of message beginning,
446 * 'end' => offset of message end))
447 */
448 private function &readMessages(array $ids, $strip = false, $from = false)
449 {
7a5823f9 450 if ($this->messages) {
7027794f 451 return $this->messages;
452 }
453 sort($ids);
454 $messages = array();
455 while ((count($ids) || $from) && !feof($this->file)) {
456 if (count($ids)) {
457 $id = array_shift($ids);
458 } else {
459 $id++;
460 }
461 if ($id != $this->current_id || !$this->at_beginning) {
462 if (!$this->goTo($id)) {
06ba62e1 463 if (count($ids)) {
464 continue;
465 } else {
466 break;
467 }
7027794f 468 }
469 }
470 $beginning = ftell($this->file);
471 $message =& $this->readCurrentMessage($strip, false);
472 if ($strip) {
473 $message =& BananaMimePart::parseHeaders($message);
474 }
475 $end = ftell($this->file);
476 $messages[$id] = array('message' => $message, 'beginning' => $beginning, 'end' => $end);
477 }
478 return $messages;
479 }
480}
481
598a1c53 482// vim:set et sw=4 sts=4 ts=4 enc=utf-8:
7027794f 483?>