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