Added a comma after each msgnum exported. The parser was globbing them all together...
[citadel.git] / ctdlphp / z-push / citadel.php
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.
18 *
19 * Created   :   01.10.2007
20 *
21 * © Zarafa Deutschland GmbH, www.zarafaserver.de
22 * This file is distributed under GPL v2.
23 * Consult LICENSE file for details
24 ************************************************/
25
26 include_once('diffbackend.php');
27
28 // The is an improved version of mimeDecode from PEAR that correctly
29 // handles charsets and charset conversion
30 include_once('mimeDecode.php');
31
32 include_once('ctdlprotocol.php');
33
34 include_once('ctdlsession.php');
35
36
37 class BackendCitadel extends BackendDiff 
38 {
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_*
43      */
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];
56     }
57
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;
73     }
74     
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.
81      */
82     function SendMessage($rfc822) {
83             debugLog("SendMessage\n");
84         // Unimplemented
85         return true;
86     }
87     
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
92      */
93     function GetWasteBasket() {
94             debugLog("GetWasteBasket");
95         return "Trash";
96     }
97     
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.
102      *
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.
107      */
108     function GetMessageList($folderid, $cutoffdate) {
109             debugLog("GetMessageList $folderid $cutoffdate");
110 ///         $this->moveNewToCur();
111
112             ctdl_goto ($folderid);
113         
114 #        if($folderid != "root")
115 #           return false;
116             
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)
121         
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.
128         
129             $message = ctdl_msgs("","");
130             debugLog(print_r($message, true), true);
131             $messages = array();
132             
133             if ($message[0] > 0) for ($i=0; $i < $message[0]; $i ++)
134             {
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);
139                     
140             }
141             return $messages;
142 //        $messages = array();
143 //        $dirname = $this->getPath();
144 //        
145 //        $dir = opendir($dirname);
146 //        
147 //        if(!$dir)
148 //            return false;
149 //        
150 //        while($entry = readdir($dir)) {
151 //            if($entry{0} == ".")
152 //                continue;
153 //                
154 //            $message = array();
155 //            
156 //            $stat = stat("$dirname/$entry");
157 //
158 //            if($stat["mtime"] < $cutoffdate) {
159 //                // message is out of range for curoffdate, ignore it
160 //                continue;
161 //            }
162 //                            
163 //            $message["mod"] = $stat["mtime"];
164 //            
165 //            $matches = array();
166 //            
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;
172 //            
173 //            if(strpos($matches[2],"S") !== false) {
174 //                $message["flags"] |= 1; // 'seen' aka 'read' is the only flag we want to know about
175 //            }
176 //            
177 //            array_push($messages, $message);
178 //        }
179 //        
180 //        return $messages;
181     }
182     
183     /* This function is analogous to GetMessageList. In simple implementations like this one,
184      * you probably just return one folder.
185      */
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])
191             {
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'))
196                             {
197                                     $folders[] = array("id"     => $folder['name'], 
198                                                        "parent" => $folder['floor'], 
199                                                        "mod"    => "Inbox");
200                                     
201                             }
202                     }
203                     return $folders;
204             }
205             else return false;
206             
207         
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;
223     }
224     
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. 
227      */
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'])
237             {
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;
256             }
257             return $box;
258 //        if($id == "root") {
259 //            $inbox = new SyncFolder();
260 //            
261 //            $inbox->serverid = $id;
262 //            $inbox->parentid = "0"; // Root
263 //            $inbox->displayname = "Inbox";
264 //            $inbox->type = SYNC_FOLDER_TYPE_INBOX;
265 //            
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;
273 //            
274 //            return $inbox;
275 //        } else {
276 //            return false;
277 //        }
278     }
279     
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)
288      */
289     function StatFolder($id) {
290 debugLog("Statfolder $id");
291         $folder = $this->GetFolder($id);
292         
293         $stat = array();
294         $stat["id"] = $id;
295         $stat["parent"] = $folder->parentid;
296         $stat["mod"] = $folder->displayname;
297         
298         return $stat;
299     }
300
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.
304      */    
305     function GetAttachmentData($attname) {
306 debugLog("GetAttachmentData");
307         list($id, $part) = explode(":", $attname);
308         
309         $fn = $this->findMessage($id);
310         
311         // Parse e-mail
312         $rfc822 = file_get_contents($this->getPath() . "/$fn");
313         
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;
316     }
317
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.
324      */
325      
326     function StatMessage($folderid, $id) {
327             debugLog("StatMessage $folderid $id");
328             return array ("id" => "$id", "flags" => 0, "mod", "12345");
329 //
330 //        $dirname = $this->getPath();
331 //        $fn = $this->findMessage($id);
332 //        if(!$fn)
333 //            return false;
334 //
335 //        $stat = stat("$dirname/$fn");
336 //
337 //        $entry = array();
338 //        $entry["id"] = $id;
339 //        $entry["flags"] = 0;
340 //
341 //        if(strpos($fn,"S"))
342 //            $entry["flags"] |= 1;
343 //        $entry["mod"] = $stat["mtime"];
344 //                
345 //        return $entry;
346     }
347     
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.
353      */
354     function GetMessage($folderid, $id) {
355             debugLog("GetMessge $folderid $id");
356 #        if($folderid != 'root')
357 #            return false;
358             
359 //        $fn = $this->findMessage($id);
360
361         // Get flags, etc
362         $stat = $this->StatMessage($folderid, $id);
363         
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();
371
372         debugLog(print_r($message, true));
373         $output = new SyncMail();
374
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;
388
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);
396                     
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";
404                     
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'] : "";
409                     
410                     array_push($output->attachments, $attachment);
411                 }
412                 $n++;
413             }
414         }
415         
416         return $output;
417     }
418     
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
424      */
425     function DeleteMessage($folderid, $id) {
426             debugLog("DeleteMessage");
427         if($folderid != 'root')
428             return false;
429             
430         $fn = $this->findMessage($id);
431
432         if(!$fn)
433             return true; // success because message has been deleted already
434
435         
436         if(!unlink($this->getPath() . "/$fn")) {
437             return true; // success - message may have been deleted in the mean time (since findMessage)
438         }
439
440         return true;
441     }
442     
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
449      */
450     function SetReadFlag($folderid, $id, $flags) {
451             debugLog("SetReadFlag");
452         if($folderid != 'root')
453             return false;
454             
455         $fn = $this->findMessage($id);
456         
457         if(!$fn)
458             return true; // message may have been deleted
459         
460         if(!preg_match("/([^:]+):2,([PRSTDF]*)/",$fn,$matches))
461             return false;
462
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";
469         }
470         
471         $newfn = $matches[1] . ":2," . $newflags;
472         // rename if required
473         if($fn != $newfn) 
474             rename($this->getPath() ."/$fn", $this->getPath() . "/$newfn");
475         
476         return true;
477     }
478     
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'.
485      */
486     function ChangeMessage($folderid, $id, $message) {
487             debugLog("ChangeMessage");
488         return false;
489     }
490     
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
495      */
496     function MoveMessage($folderid, $id, $newfolderid) {
497             debugLog("MoveMessage");
498         return false;
499     }
500
501     // ----------------------------------------
502     // maildir-specific internals
503     
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.
509
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);
517 //        
518 //        while($entry = readdir($dir)) {
519 //            if(strpos($entry,$id) === 0)
520 //                return $entry;
521 //        }
522 //        return false; // not found
523     }
524     
525     /* Parse the message and return only the plaintext body
526      */
527     function getBody($message) {
528             debugLog("getBody -> $message <-");
529         $body = "";
530         $htmlbody = "";
531         
532         $this->getBodyRecursive($message, "plain", $body);
533         
534         if(!isset($body) || $body === "") {
535             $this->getBodyRecursive($message, "html", $body);
536             // HTML conversion goes here
537         }
538         
539         return $body;
540     }
541     
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;
548         
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);
553                 }
554             }
555         }
556     }
557
558     function parseReceivedDate($received) {
559             debugLog("parseRecivedDate");
560             $pos = strpos($received, ";");
561         if(!$pos)
562             return false;
563             
564         $datestr = substr($received, $pos+1);
565         $datestr = ltrim($datestr);
566         
567         return strtotime($datestr);
568     }
569     
570     /* moves everything in Maildir/new/* to Maildir/cur/
571      */
572     function moveNewToCur() {
573             debugLog("moveNewToCur");
574         $newdirname = MAILDIR_BASE . "/" . $this->_user . "/" . MAILDIR_SUBDIR . "/new";
575         
576         $newdir = opendir($newdirname);
577         
578         while($newentry = readdir($newdir)) {
579             if($newentry{0} == ".")
580                 continue;
581                 
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);
585         }
586     }
587     
588     /* The path we're working on
589      */
590     function getPath() {
591             debugLog("GetPath");
592         return MAILDIR_BASE . "/" . $this->_user . "/" . MAILDIR_SUBDIR . "/cur";
593     }
594 };
595
596
597 ?>