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