* The code which loads expire policies had been incompletely ported to strbuf, causin...
[citadel.git] / ctdlphp / z-push / citadel.php
blob00a9ff776b69d9aebe9a17b0fe409cfc5295d12a
1 <?
2 /***********************************************
3 * File : maildir.php
4 * Project : Z-Push
5 * Descr : This backend is based on
6 * 'BackendDiff' which handles the
7 * intricacies of generating
8 * differentials from static
9 * snapshots. This means that the
10 * implementation here needs no
11 * state information, and can simply
12 * return the current state of the
13 * messages. The diffbackend will
14 * then compare the current state
15 * to the known last state of the PDA
16 * and generate change increments
17 * from that.
19 * Created : 01.10.2007
21 * © Zarafa Deutschland GmbH, www.zarafaserver.de
22 * This file is distributed under GPL v2.
23 * Consult LICENSE file for details
24 ************************************************/
26 include_once('diffbackend.php');
28 // The is an improved version of mimeDecode from PEAR that correctly
29 // handles charsets and charset conversion
30 include_once('mimeDecode.php');
32 include_once('ctdlprotocol.php');
34 include_once('ctdlsession.php');
37 class BackendCitadel extends BackendDiff
39 /* Called to logon a user. These are the three authentication strings that you must
40 * specify in ActiveSync on the PDA. Normally you would do some kind of password
41 * check here. Alternatively, you could ignore the password here and have Apache
42 * do authentication via mod_auth_*
44 function Logon($username, $domain, $password) {
45 debugLog ("Logging in.\n");
46 establish_citadel_session();
47 $usr = explode ('\\', $username);
48 /// debugLog(print_r($usr, true));
49 debugLog($password);
50 if (count ($usr) == 2)
51 $username = $usr[1];
52 $ret = login_existing_user($username, $password);
53 if ($ret[0] != TRUE)
54 echo $ret[1];
55 return $ret[0];
58 /* Called directly after the logon. This specifies the client's protocol version
59 * and device id. The device ID can be used for various things, including saving
60 * per-device state information.
61 * The $user parameter here is normally equal to the $username parameter from the
62 * Logon() call. In theory though, you could log on a 'foo', and then sync the emails
63 * of user 'bar'. The $user here is the username specified in the request URL, while the
64 * $username in the Logon() call is the username which was sent as a part of the HTTP
65 * authentication.
66 */
67 function Setup($user, $devid, $protocolversion) {
68 debugLog ("Setup\n");
69 $this->_user = $user;
70 $this->_devid = $devid;
71 $this->_protocolversion = $protocolversion;
72 return true;
75 /* Sends a message which is passed as rfc822. You basically can do two things
76 * 1) Send the message to an SMTP server as-is
77 * 2) Parse the message yourself, and send it some other way
78 * It is up to you whether you want to put the message in the sent items folder. If you
79 * want it in 'sent items', then the next sync on the 'sent items' folder should return
80 * the new message as any other new message in a folder.
82 function SendMessage($rfc822) {
83 debugLog("SendMessage\n");
84 // Unimplemented
85 return true;
88 /* Should return a wastebasket folder if there is one. This is used when deleting
89 * items; if this function returns a valid folder ID, then all deletes are handled
90 * as moves and are sent to your backend as a move. If it returns FALSE, then deletes
91 * are always handled as real deletes and will be sent to your importer as a DELETE
93 function GetWasteBasket() {
94 debugLog("GetWasteBasket");
95 return "Trash";
98 /* Should return a list (array) of messages, each entry being an associative array
99 * with the same entries as StatMessage(). This function should return stable information; ie
100 * if nothing has changed, the items in the array must be exactly the same. The order of
101 * the items within the array is not important though.
103 * The cutoffdate is a date in the past, representing the date since which items should be shown.
104 * This cutoffdate is determined by the user's setting of getting 'Last 3 days' of e-mail, etc. If
105 * you ignore the cutoffdate, the user will not be able to select their own cutoffdate, but all
106 * will work OK apart from that.
108 function GetMessageList($folderid, $cutoffdate) {
109 debugLog("GetMessageList $folderid $cutoffdate");
110 /// $this->moveNewToCur();
112 ctdl_goto ($folderid);
114 # if($folderid != "root")
115 # return false;
117 // return stats of all messages in a dir. We can do this faster than
118 // just calling statMessage() on each message; We still need fstat()
119 // information though, so listing 10000 messages is going to be
120 // rather slow (depending on filesystem, etc)
122 // we also have to filter by the specified cutoffdate so only the
123 // last X days are retrieved. Normally, this would mean that we'd
124 // have to open each message, get the Received: header, and check
125 // whether that is in the filter range. Because this is much too slow, we
126 // are depending on the creation date of the message instead, which should
127 // normally be just about the same, unless you just did some kind of import.
129 $message = ctdl_msgs("","");
130 debugLog(print_r($message, true), true);
131 $messages = array();
133 if ($message[0] > 0) for ($i=0; $i < $message[0]; $i ++)
135 $thismessage["id"] = $message[2][$i];
136 $thismessage["flags"] = 0;
137 $thismessage["flags"] |= 1; // 'seen' aka 'read' is the only flag we want to know about
138 array_push($messages, $thismessage);
141 return $messages;
142 // $messages = array();
143 // $dirname = $this->getPath();
145 // $dir = opendir($dirname);
147 // if(!$dir)
148 // return false;
150 // while($entry = readdir($dir)) {
151 // if($entry{0} == ".")
152 // continue;
154 // $message = array();
156 // $stat = stat("$dirname/$entry");
158 // if($stat["mtime"] < $cutoffdate) {
159 // // message is out of range for curoffdate, ignore it
160 // continue;
161 // }
163 // $message["mod"] = $stat["mtime"];
165 // $matches = array();
167 // // Flags according to http://cr.yp.to/proto/maildir.html (pretty authoritative - qmail author's website)
168 // if(!preg_match("/([^:]+):2,([PRSTDF]*)/",$entry,$matches))
169 // continue;
170 // $message["id"] = $matches[1];
171 // $message["flags"] = 0;
173 // if(strpos($matches[2],"S") !== false) {
174 // $message["flags"] |= 1; // 'seen' aka 'read' is the only flag we want to know about
175 // }
177 // array_push($messages, $message);
178 // }
180 // return $messages;
183 /* This function is analogous to GetMessageList. In simple implementations like this one,
184 * you probably just return one folder.
186 function GetFolderList() {
187 $folders = array();
188 debugLog("GetFolderList");
189 $ret = ctdl_knrooms(); /// TODO: should we just get the rooms with new messages in them? No.
190 if ($ret[0])
192 $fldr = $ret[1];
193 foreach ($fldr as $folder)
194 { // hide contacts and calendar here... TODO: do we realy need to?
195 if (($folder['name'] != 'Calendar') && ($folder['name'] != 'Contacts'))
197 $folders[] = array("id" => $folder['name'],
198 "parent" => $folder['floor'],
199 "mod" => "Inbox");
203 return $folders;
205 else return false;
208 /// $inbox = array();
209 /// $inbox["id"] = "root";
210 /// $inbox["parent"] = "0";
211 /// $inbox["mod"] = "Inbox";
212 ///
213 /// $folders[]=$inbox;
214 ///
215 /// $sub = array();
216 /// $sub["id"] = "sub";
217 /// $sub["parent"] = "root";
218 /// $sub["mod"] = "Sub";
219 ///
220 ///// $folders[]=$sub;
221 ///
222 /// return $folders;
225 /* GetFolder should return an actual SyncFolder object with all the properties set. Folders
226 * are pretty simple really, having only a type, a name, a parent and a server ID.
228 function GetFolder($id) {
229 debugLog("GetFolder $id");
230 $ret = ctdl_goto ($id);
231 // debugLog(print_r($ret, true));
232 $box = new SyncFolder();
233 $box->serverid = $id;
234 $box->parentid = $ret['floorid'];
235 $box->displayname = $ret['roomname'];
236 switch ($ret['defaultview'])
238 case VIEW_BBS:
239 $box->type = SYNC_FOLDER_TYPE_OTHER;
240 break;
241 case VIEW_MAILBOX:
242 $box->type = SYNC_FOLDER_TYPE_INBOX;
243 break;
244 case VIEW_ADDRESSBOOK:
245 $box->type = SYNC_FOLDER_TYPE_OTHER;
246 break;
247 case VIEW_CALENDAR:
248 $box->type = SYNC_FOLDER_TYPE_OTHER;
249 break;
250 case VIEW_TASKS:
251 $box->type = SYNC_FOLDER_TYPE_OTHER;
252 break;
253 case VIEW_NOTES:
254 $box->type = SYNC_FOLDER_TYPE_OTHER;
255 break;
257 return $box;
258 // if($id == "root") {
259 // $inbox = new SyncFolder();
261 // $inbox->serverid = $id;
262 // $inbox->parentid = "0"; // Root
263 // $inbox->displayname = "Inbox";
264 // $inbox->type = SYNC_FOLDER_TYPE_INBOX;
266 // return $inbox;
267 // } else if($id = "sub") {
268 // $inbox = new SyncFolder();
269 // $inbox->serverid = $id;
270 // $inbox->parentid = "root";
271 // $inbox->displayname = "Sub";
272 // $inbox->type = SYNC_FOLDER_TYPE_OTHER;
274 // return $inbox;
275 // } else {
276 // return false;
277 // }
280 /* Return folder stats. This means you must return an associative array with the
281 * following properties:
282 * "id" => The server ID that will be used to identify the folder. It must be unique, and not too long
283 * How long exactly is not known, but try keeping it under 20 chars or so. It must be a string.
284 * "parent" => The server ID of the parent of the folder. Same restrictions as 'id' apply.
285 * "mod" => This is the modification signature. It is any arbitrary string which is constant as long as
286 * the folder has not changed. In practice this means that 'mod' can be equal to the folder name
287 * as this is the only thing that ever changes in folders. (the type is normally constant)
289 function StatFolder($id) {
290 debugLog("Statfolder $id");
291 $folder = $this->GetFolder($id);
293 $stat = array();
294 $stat["id"] = $id;
295 $stat["parent"] = $folder->parentid;
296 $stat["mod"] = $folder->displayname;
298 return $stat;
301 /* Should return attachment data for the specified attachment. The passed attachment identifier is
302 * the exact string that is returned in the 'AttName' property of an SyncAttachment. So, you should
303 * encode any information you need to find the attachment in that 'attname' property.
305 function GetAttachmentData($attname) {
306 debugLog("GetAttachmentData");
307 list($id, $part) = explode(":", $attname);
309 $fn = $this->findMessage($id);
311 // Parse e-mail
312 $rfc822 = file_get_contents($this->getPath() . "/$fn");
314 $message = Mail_mimeDecode::decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'input' => $rfc822, 'crlf' => "\n", 'charset' => 'utf-8'));
315 return $message->parts[$part]->body;
318 /* StatMessage should return message stats, analogous to the folder stats (StatFolder). Entries are:
319 * 'id' => Server unique identifier for the message. Again, try to keep this short (under 20 chars)
320 * 'flags' => simply '0' for unread, '1' for read
321 * 'mod' => modification signature. As soon as this signature changes, the item is assumed to be completely
322 * changed, and will be sent to the PDA as a whole. Normally you can use something like the modification
323 * time for this field, which will change as soon as the contents have changed.
326 function StatMessage($folderid, $id) {
327 debugLog("StatMessage $folderid $id");
328 return array ("id" => "$id", "flags" => 0, "mod", "12345");
330 // $dirname = $this->getPath();
331 // $fn = $this->findMessage($id);
332 // if(!$fn)
333 // return false;
335 // $stat = stat("$dirname/$fn");
337 // $entry = array();
338 // $entry["id"] = $id;
339 // $entry["flags"] = 0;
341 // if(strpos($fn,"S"))
342 // $entry["flags"] |= 1;
343 // $entry["mod"] = $stat["mtime"];
345 // return $entry;
348 /* GetMessage should return the actual SyncXXX object type. You may or may not use the '$folderid' parent folder
349 * identifier here.
350 * Note that mixing item types is illegal and will be blocked by the engine; ie returning an Email object in a
351 * Tasks folder will not do anything. The SyncXXX objects should be filled with as much information as possible,
352 * but at least the subject, body, to, from, etc.
354 function GetMessage($folderid, $id) {
355 debugLog("GetMessge $folderid $id");
356 # if($folderid != 'root')
357 # return false;
359 // $fn = $this->findMessage($id);
361 // Get flags, etc
362 $stat = $this->StatMessage($folderid, $id);
364 // Parse e-mail
365 $rfc822 = $this->findMessage($id);
366 #file_get_contents($this->getPath() . "/" . $fn);
367 debugLog("-------------------".print_r($rfc822, true));
368 $params = array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'crlf' => "\r\n", 'charset' => 'utf-8');
369 $decoder = new Mail_mimeDecode($rfc822);
370 $message = $decoder->decode();
372 debugLog(print_r($message, true));
373 $output = new SyncMail();
375 $output->body = str_replace("\n", "\r\n", $this->getBody($message));
376 $output->bodysize = strlen($output->body);
377 $output->bodytruncated = 0;
378 $output->datereceived = $this->parseReceivedDate($message->headers["received"][0]);
379 $output->displayto = $message->headers["to"];
380 $output->importance = $message->headers["x-priority"];
381 $output->messageclass = "IPM.Note";
382 $output->subject = $message->headers["subject"];
383 $output->read = $stat["flags"];
384 $output->to = $message->headers["to"];
385 $output->cc = $message->headers["cc"];
386 $output->from = $message->headers["from"];
387 $output->reply_to = isset($message->headers["reply-to"]) ? $message->headers["reply-to"] : null;
389 // Attachments are only searched in the top-level part
390 $n = 0;
391 if(isset($message->parts)) {
392 foreach($message->parts as $part) {
393 if($part->ctype_primary == "application") {
394 $attachment = new SyncAttachment();
395 $attachment->attsize = strlen($part->body);
397 if(isset($part->d_parameters['filename']))
398 $attname = $part->d_parameters['filename'];
399 else if(isset($part->ctype_parameters['name']))
400 $attname = $part->ctype_parameters['name'];
401 else if(isset($part->headers['content-description']))
402 $attname = $part->headers['content-description'];
403 else $attname = "unknown attachment";
405 $attachment->displayname = $attname;
406 $attachment->attname = $id . ":" . $n;
407 $attachment->attmethod = 1;
408 $attachment->attoid = isset($part->headers['content-id']) ? $part->headers['content-id'] : "";
410 array_push($output->attachments, $attachment);
412 $n++;
416 return $output;
419 /* This function is called when the user has requested to delete (really delete) a message. Usually
420 * this means just unlinking the file its in or somesuch. After this call has succeeded, a call to
421 * GetMessageList() should no longer list the message. If it does, the message will be re-sent to the PDA
422 * as it will be seen as a 'new' item. This means that if you don't implement this function, you will
423 * be able to delete messages on the PDA, but as soon as you sync, you'll get the item back
425 function DeleteMessage($folderid, $id) {
426 debugLog("DeleteMessage");
427 if($folderid != 'root')
428 return false;
430 $fn = $this->findMessage($id);
432 if(!$fn)
433 return true; // success because message has been deleted already
436 if(!unlink($this->getPath() . "/$fn")) {
437 return true; // success - message may have been deleted in the mean time (since findMessage)
440 return true;
443 /* This should change the 'read' flag of a message on disk. The $flags
444 * parameter can only be '1' (read) or '0' (unread). After a call to
445 * SetReadFlag(), GetMessageList() should return the message with the
446 * new 'flags' but should not modify the 'mod' parameter. If you do
447 * change 'mod', simply setting the message to 'read' on the PDA will trigger
448 * a full resync of the item from the server
450 function SetReadFlag($folderid, $id, $flags) {
451 debugLog("SetReadFlag");
452 if($folderid != 'root')
453 return false;
455 $fn = $this->findMessage($id);
457 if(!$fn)
458 return true; // message may have been deleted
460 if(!preg_match("/([^:]+):2,([PRSTDF]*)/",$fn,$matches))
461 return false;
463 // remove 'seen' (S) flag
464 if(!$flags) {
465 $newflags = str_replace("S","",$matches[2]);
466 } else {
467 // make sure we don't double add the 'S' flag
468 $newflags = str_replace("S","",$matches[2]) . "S";
471 $newfn = $matches[1] . ":2," . $newflags;
472 // rename if required
473 if($fn != $newfn)
474 rename($this->getPath() ."/$fn", $this->getPath() . "/$newfn");
476 return true;
479 /* This function is called when a message has been changed on the PDA. You should parse the new
480 * message here and save the changes to disk. The return value must be whatever would be returned
481 * from StatMessage() after the message has been saved. This means that both the 'flags' and the 'mod'
482 * properties of the StatMessage() item may change via ChangeMessage().
483 * Note that this function will never be called on E-mail items as you can't change e-mail items, you
484 * can only set them as 'read'.
486 function ChangeMessage($folderid, $id, $message) {
487 debugLog("ChangeMessage");
488 return false;
491 /* This function is called when the user moves an item on the PDA. You should do whatever is needed
492 * to move the message on disk. After this call, StatMessage() and GetMessageList() should show the items
493 * to have a new parent. This means that it will disappear from GetMessageList() will not return the item
494 * at all on the source folder, and the destination folder will show the new message
496 function MoveMessage($folderid, $id, $newfolderid) {
497 debugLog("MoveMessage");
498 return false;
501 // ----------------------------------------
502 // maildir-specific internals
504 function findMessage($id) {
505 debugLog("findMessage $id");
506 // We could use 'this->_folderid' for path info but we currently
507 // only support a single INBOX. We also have to use a glob '*'
508 // because we don't know the flags of the message we're looking for.
510 $msg = ctdl_fetch_message_rfc822($id);
511 if ($msg[0])
512 return $msg[1];
513 else
514 return false;
515 // $dirname = $this->getPath();
516 // $dir = opendir($dirname);
518 // while($entry = readdir($dir)) {
519 // if(strpos($entry,$id) === 0)
520 // return $entry;
521 // }
522 // return false; // not found
525 /* Parse the message and return only the plaintext body
527 function getBody($message) {
528 debugLog("getBody -> $message <-");
529 $body = "";
530 $htmlbody = "";
532 $this->getBodyRecursive($message, "plain", $body);
534 if(!isset($body) || $body === "") {
535 $this->getBodyRecursive($message, "html", $body);
536 // HTML conversion goes here
539 return $body;
542 // Get all parts in the message with specified type and concatenate them together, unless the
543 // Content-Disposition is 'attachment', in which case the text is apparently an attachment
544 function getBodyRecursive($message, $subtype, &$body) {
545 debugLog("GetBodyRecursive $subtype".print_r($message, true));
546 if(strcasecmp($message->ctype_primary,"text")==0 && strcasecmp($message->ctype_secondary,$subtype)==0 && isset($message->body))
547 $body .= $message->body;
549 if(strcasecmp($message->ctype_primary,"multipart")==0) {
550 foreach($message->parts as $part) {
551 if(!isset($part->disposition) || strcasecmp($part->disposition,"attachment")) {
552 $this->getBodyRecursive($part, $subtype, $body);
558 function parseReceivedDate($received) {
559 debugLog("parseRecivedDate");
560 $pos = strpos($received, ";");
561 if(!$pos)
562 return false;
564 $datestr = substr($received, $pos+1);
565 $datestr = ltrim($datestr);
567 return strtotime($datestr);
570 /* moves everything in Maildir/new/* to Maildir/cur/
572 function moveNewToCur() {
573 debugLog("moveNewToCur");
574 $newdirname = MAILDIR_BASE . "/" . $this->_user . "/" . MAILDIR_SUBDIR . "/new";
576 $newdir = opendir($newdirname);
578 while($newentry = readdir($newdir)) {
579 if($newentry{0} == ".")
580 continue;
582 // link/unlink == move. This is the way to move the message according to cr.yp.to
583 link($newdirname . "/" . $newentry, $this->getPath() . "/" . $newentry . ":2,");
584 unlink($newdirname . "/" . $newentry);
588 /* The path we're working on
590 function getPath() {
591 debugLog("GetPath");
592 return MAILDIR_BASE . "/" . $this->_user . "/" . MAILDIR_SUBDIR . "/cur";