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