source: trunk/zpush/backend/maildir.php @ 7589

Revision 7589, 23.3 KB checked in by douglas, 11 years ago (diff)

Ticket #3209 - Integrar módulo de sincronização Z-push ao Expresso

Line 
1<?php
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* Copyright 2007 - 2012 Zarafa Deutschland GmbH
22*
23* This program is free software: you can redistribute it and/or modify
24* it under the terms of the GNU Affero General Public License, version 3,
25* as published by the Free Software Foundation with the following additional
26* term according to sec. 7:
27*
28* According to sec. 7 of the GNU Affero General Public License, version 3,
29* the terms of the AGPL are supplemented with the following terms:
30*
31* "Zarafa" is a registered trademark of Zarafa B.V.
32* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
33* The licensing of the Program under the AGPL does not imply a trademark license.
34* Therefore any rights, title and interest in our trademarks remain entirely with us.
35*
36* However, if you propagate an unmodified version of the Program you are
37* allowed to use the term "Z-Push" to indicate that you distribute the Program.
38* Furthermore you may use our trademarks where it is necessary to indicate
39* the intended purpose of a product or service provided you use it in accordance
40* with honest practices in industrial or commercial matters.
41* If you want to propagate modified versions of the Program under the name "Z-Push",
42* you may only do so if you have a written permission by Zarafa Deutschland GmbH
43* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
44*
45* This program is distributed in the hope that it will be useful,
46* but WITHOUT ANY WARRANTY; without even the implied warranty of
47* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
48* GNU Affero General Public License for more details.
49*
50* You should have received a copy of the GNU Affero General Public License
51* along with this program.  If not, see <http://www.gnu.org/licenses/>.
52*
53* Consult LICENSE file for details
54************************************************/
55
56include_once('lib/default/diffbackend/diffbackend.php');
57include_once('include/mimeDecode.php');
58require_once('include/z_RFC822.php');
59
60class BackendMaildir extends BackendDiff {
61    /**----------------------------------------------------------------------------------------------------------
62     * default backend methods
63     */
64
65    /**
66     * Authenticates the user - NOT EFFECTIVELY IMPLEMENTED
67     * Normally some kind of password check would be done here.
68     * Alternatively, the password could be ignored and an Apache
69     * authentication via mod_auth_* could be done
70     *
71     * @param string        $username
72     * @param string        $domain
73     * @param string        $password
74     *
75     * @access public
76     * @return boolean
77     */
78    public function Logon($username, $domain, $password) {
79        return true;
80    }
81
82    /**
83     * Logs off
84     *
85     * @access public
86     * @return boolean
87     */
88    public function Logoff() {
89        return true;
90    }
91
92    /**
93     * Sends an e-mail
94     * Not implemented here
95     *
96     * @param SyncSendMail  $sm     SyncSendMail object
97     *
98     * @access public
99     * @return boolean
100     * @throws StatusException
101     */
102    public function SendMail($sm) {
103        return false;
104    }
105
106    /**
107     * Returns the waste basket
108     *
109     * @access public
110     * @return string
111     */
112    public function GetWasteBasket() {
113        return false;
114    }
115
116    /**
117     * Returns the content of the named attachment as stream. The passed attachment identifier is
118     * the exact string that is returned in the 'AttName' property of an SyncAttachment.
119     * Any information necessary to find the attachment must be encoded in that 'attname' property.
120     * Data is written directly (with print $data;)
121     *
122     * @param string        $attname
123     *
124     * @access public
125     * @return SyncItemOperationsAttachment
126     * @throws StatusException
127     */
128    public function GetAttachmentData($attname) {
129        list($id, $part) = explode(":", $attname);
130
131        $fn = $this->findMessage($id);
132        if ($fn == false)
133            throw new StatusException(sprintf("BackendMaildir->GetAttachmentData('%s'): Error, requested message/attachment can not be found", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
134
135        // Parse e-mail
136        $rfc822 = file_get_contents($this->getPath() . "/$fn");
137
138        $message = Mail_mimeDecode::decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'input' => $rfc822, 'crlf' => "\n", 'charset' => 'utf-8'));
139
140        include_once('include/stringstreamwrapper.php');
141        $attachment = new SyncItemOperationsAttachment();
142        $attachment->data = StringStreamWrapper::Open($message->parts[$part]->body);
143        if (isset($message->parts[$part]->ctype_primary) && isset($message->parts[$part]->ctype_secondary))
144            $attachment->contenttype = $message->parts[$part]->ctype_primary .'/'.$message->parts[$part]->ctype_secondary;
145
146        return $attachment;
147    }
148
149    /**----------------------------------------------------------------------------------------------------------
150     * implemented DiffBackend methods
151     */
152
153
154    /**
155     * Returns a list (array) of folders.
156     * In simple implementations like this one, probably just one folder is returned.
157     *
158     * @access public
159     * @return array
160     */
161    public function GetFolderList() {
162        $folders = array();
163
164        $inbox = array();
165        $inbox["id"] = "root";
166        $inbox["parent"] = "0";
167        $inbox["mod"] = "Inbox";
168
169        $folders[]=$inbox;
170
171        $sub = array();
172        $sub["id"] = "sub";
173        $sub["parent"] = "root";
174        $sub["mod"] = "Sub";
175
176//        $folders[]=$sub;
177
178        return $folders;
179    }
180
181    /**
182     * Returns an actual SyncFolder object
183     *
184     * @param string        $id           id of the folder
185     *
186     * @access public
187     * @return object       SyncFolder with information
188     */
189    public function GetFolder($id) {
190        if($id == "root") {
191            $inbox = new SyncFolder();
192
193            $inbox->serverid = $id;
194            $inbox->parentid = "0"; // Root
195            $inbox->displayname = "Inbox";
196            $inbox->type = SYNC_FOLDER_TYPE_INBOX;
197
198            return $inbox;
199        } else if($id == "sub") {
200            $inbox = new SyncFolder();
201            $inbox->serverid = $id;
202            $inbox->parentid = "root";
203            $inbox->displayname = "Sub";
204            $inbox->type = SYNC_FOLDER_TYPE_OTHER;
205
206            return $inbox;
207        } else {
208            return false;
209        }
210    }
211
212
213    /**
214     * Returns folder stats. An associative array with properties is expected.
215     *
216     * @param string        $id             id of the folder
217     *
218     * @access public
219     * @return array
220     */
221    public function StatFolder($id) {
222        $folder = $this->GetFolder($id);
223
224        $stat = array();
225        $stat["id"] = $id;
226        $stat["parent"] = $folder->parentid;
227        $stat["mod"] = $folder->displayname;
228
229        return $stat;
230    }
231
232
233    /**
234     * Creates or modifies a folder
235     * not implemented
236     *
237     * @param string        $folderid       id of the parent folder
238     * @param string        $oldid          if empty -> new folder created, else folder is to be renamed
239     * @param string        $displayname    new folder name (to be created, or to be renamed to)
240     * @param int           $type           folder type
241     *
242     * @access public
243     * @return boolean                      status
244     * @throws StatusException              could throw specific SYNC_FSSTATUS_* exceptions
245     *
246     */
247    public function ChangeFolder($folderid, $oldid, $displayname, $type){
248        return false;
249    }
250
251    /**
252     * Deletes a folder
253     *
254     * @param string        $id
255     * @param string        $parent         is normally false
256     *
257     * @access public
258     * @return boolean                      status - false if e.g. does not exist
259     * @throws StatusException              could throw specific SYNC_FSSTATUS_* exceptions
260     *
261     */
262    public function DeleteFolder($id, $parentid){
263        return false;
264    }
265
266    /**
267     * Returns a list (array) of messages
268     *
269     * @param string        $folderid       id of the parent folder
270     * @param long          $cutoffdate     timestamp in the past from which on messages should be returned
271     *
272     * @access public
273     * @return array/false  array with messages or false if folder is not available
274     */
275    public function GetMessageList($folderid, $cutoffdate) {
276        $this->moveNewToCur();
277
278        if($folderid != "root")
279            return false;
280
281        // return stats of all messages in a dir. We can do this faster than
282        // just calling statMessage() on each message; We still need fstat()
283        // information though, so listing 10000 messages is going to be
284        // rather slow (depending on filesystem, etc)
285
286        // we also have to filter by the specified cutoffdate so only the
287        // last X days are retrieved. Normally, this would mean that we'd
288        // have to open each message, get the Received: header, and check
289        // whether that is in the filter range. Because this is much too slow, we
290        // are depending on the creation date of the message instead, which should
291        // normally be just about the same, unless you just did some kind of import.
292
293        $messages = array();
294        $dirname = $this->getPath();
295
296        $dir = opendir($dirname);
297
298        if(!$dir)
299            return false;
300
301        while($entry = readdir($dir)) {
302            if($entry{0} == ".")
303                continue;
304
305            $message = array();
306
307            $stat = stat("$dirname/$entry");
308
309            if($stat["mtime"] < $cutoffdate) {
310                // message is out of range for curoffdate, ignore it
311                continue;
312            }
313
314            $message["mod"] = $stat["mtime"];
315
316            $matches = array();
317
318            // Flags according to http://cr.yp.to/proto/maildir.html (pretty authoritative - qmail author's website)
319            if(!preg_match("/([^:]+):2,([PRSTDF]*)/",$entry,$matches))
320                continue;
321            $message["id"] = $matches[1];
322            $message["flags"] = 0;
323
324            if(strpos($matches[2],"S") !== false) {
325                $message["flags"] |= 1; // 'seen' aka 'read' is the only flag we want to know about
326            }
327
328            array_push($messages, $message);
329        }
330
331        return $messages;
332    }
333
334    /**
335     * Returns the actual SyncXXX object type.
336     *
337     * @param string            $folderid           id of the parent folder
338     * @param string            $id                 id of the message
339     * @param ContentParameters $contentparameters  parameters of the requested message (truncation, mimesupport etc)
340     *
341     * @access public
342     * @return object/false     false if the message could not be retrieved
343     */
344    public function GetMessage($folderid, $id, $truncsize, $mimesupport = 0) {
345        if($folderid != 'root')
346            return false;
347
348        $fn = $this->findMessage($id);
349
350        // Get flags, etc
351        $stat = $this->StatMessage($folderid, $id);
352
353        // Parse e-mail
354        $rfc822 = file_get_contents($this->getPath() . "/" . $fn);
355
356        $message = Mail_mimeDecode::decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'input' => $rfc822, 'crlf' => "\n", 'charset' => 'utf-8'));
357
358        $output = new SyncMail();
359
360        $output->body = str_replace("\n", "\r\n", $this->getBody($message));
361        $output->bodysize = strlen($output->body);
362        $output->bodytruncated = 0; // We don't implement truncation in this backend
363        $output->datereceived = $this->parseReceivedDate($message->headers["received"][0]);
364        $output->messageclass = "IPM.Note";
365        $output->subject = $message->headers["subject"];
366        $output->read = $stat["flags"];
367        $output->from = $message->headers["from"];
368
369        $Mail_RFC822 = new Mail_RFC822();
370        $toaddr = $ccaddr = $replytoaddr = array();
371        if(isset($message->headers["to"]))
372            $toaddr = $Mail_RFC822->parseAddressList($message->headers["to"]);
373        if(isset($message->headers["cc"]))
374            $ccaddr = $Mail_RFC822->parseAddressList($message->headers["cc"]);
375        if(isset($message->headers["reply_to"]))
376            $replytoaddr = $Mail_RFC822->parseAddressList($message->headers["reply_to"]);
377
378        $output->to = array();
379        $output->cc = array();
380        $output->reply_to = array();
381        foreach(array("to" => $toaddr, "cc" => $ccaddr, "reply_to" => $replytoaddr) as $type => $addrlist) {
382            foreach($addrlist as $addr) {
383                $address = $addr->mailbox . "@" . $addr->host;
384                $name = $addr->personal;
385
386                if (!isset($output->displayto) && $name != "")
387                    $output->displayto = $name;
388
389                if($name == "" || $name == $address)
390                    $fulladdr = w2u($address);
391                else {
392                    if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') {
393                        $fulladdr = "\"" . w2u($name) ."\" <" . w2u($address) . ">";
394                    }
395                    else {
396                        $fulladdr = w2u($name) ." <" . w2u($address) . ">";
397                    }
398                }
399
400                array_push($output->$type, $fulladdr);
401            }
402        }
403
404        // convert mime-importance to AS-importance
405        if (isset($message->headers["x-priority"])) {
406            $mimeImportance =  preg_replace("/\D+/", "", $message->headers["x-priority"]);
407            if ($mimeImportance > 3)
408                $output->importance = 0;
409            if ($mimeImportance == 3)
410                $output->importance = 1;
411            if ($mimeImportance < 3)
412                $output->importance = 2;
413        }
414
415        // Attachments are only searched in the top-level part
416        $n = 0;
417        if(isset($message->parts)) {
418            foreach($message->parts as $part) {
419                if($part->ctype_primary == "application") {
420                    $attachment = new SyncAttachment();
421                    $attachment->attsize = strlen($part->body);
422
423                    if(isset($part->d_parameters['filename']))
424                        $attname = $part->d_parameters['filename'];
425                    else if(isset($part->ctype_parameters['name']))
426                        $attname = $part->ctype_parameters['name'];
427                    else if(isset($part->headers['content-description']))
428                        $attname = $part->headers['content-description'];
429                    else $attname = "unknown attachment";
430
431                    $attachment->displayname = $attname;
432                    $attachment->attname = $id . ":" . $n;
433                    $attachment->attmethod = 1;
434                    $attachment->attoid = isset($part->headers['content-id']) ? $part->headers['content-id'] : "";
435
436                    array_push($output->attachments, $attachment);
437                }
438                $n++;
439            }
440        }
441
442        return $output;
443    }
444
445    /**
446     * Returns message stats, analogous to the folder stats from StatFolder().
447     *
448     * @param string        $folderid       id of the folder
449     * @param string        $id             id of the message
450     *
451     * @access public
452     * @return array
453     */
454    public function StatMessage($folderid, $id) {
455        $dirname = $this->getPath();
456        $fn = $this->findMessage($id);
457        if(!$fn)
458            return false;
459
460        $stat = stat("$dirname/$fn");
461
462        $entry = array();
463        $entry["id"] = $id;
464        $entry["flags"] = 0;
465
466        if(strpos($fn,"S"))
467            $entry["flags"] |= 1;
468        $entry["mod"] = $stat["mtime"];
469
470        return $entry;
471    }
472
473    /**
474     * Called when a message has been changed on the mobile.
475     * This functionality is not available for emails.
476     *
477     * @param string        $folderid       id of the folder
478     * @param string        $id             id of the message
479     * @param SyncXXX       $message        the SyncObject containing a message
480     *
481     * @access public
482     * @return array                        same return value as StatMessage()
483     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
484     */
485    public function ChangeMessage($folderid, $id, $message) {
486        return false;
487    }
488
489    /**
490     * Changes the 'read' flag of a message on disk
491     *
492     * @param string        $folderid       id of the folder
493     * @param string        $id             id of the message
494     * @param int           $flags          read flag of the message
495     *
496     * @access public
497     * @return boolean                      status of the operation
498     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
499     */
500    public function SetReadFlag($folderid, $id, $flags) {
501        if($folderid != 'root')
502            return false;
503
504        $fn = $this->findMessage($id);
505
506        if(!$fn)
507            return true; // message may have been deleted
508
509        if(!preg_match("/([^:]+):2,([PRSTDF]*)/",$fn,$matches))
510            return false;
511
512        // remove 'seen' (S) flag
513        if(!$flags) {
514            $newflags = str_replace("S","",$matches[2]);
515        } else {
516            // make sure we don't double add the 'S' flag
517            $newflags = str_replace("S","",$matches[2]) . "S";
518        }
519
520        $newfn = $matches[1] . ":2," . $newflags;
521        // rename if required
522        if($fn != $newfn)
523            rename($this->getPath() ."/$fn", $this->getPath() . "/$newfn");
524
525        return true;
526    }
527
528    /**
529     * Called when the user has requested to delete (really delete) a message
530     *
531     * @param string        $folderid       id of the folder
532     * @param string        $id             id of the message
533     *
534     * @access public
535     * @return boolean                      status of the operation
536     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
537     */
538    public function DeleteMessage($folderid, $id) {
539        if($folderid != 'root')
540            return false;
541
542        $fn = $this->findMessage($id);
543
544        if(!$fn)
545            return true; // success because message has been deleted already
546
547        if(!unlink($this->getPath() . "/$fn")) {
548            return true; // success - message may have been deleted in the mean time (since findMessage)
549        }
550
551        return true;
552    }
553
554    /**
555     * Called when the user moves an item on the PDA from one folder to another
556     * not implemented
557     *
558     * @param string        $folderid       id of the source folder
559     * @param string        $id             id of the message
560     * @param string        $newfolderid    id of the destination folder
561     *
562     * @access public
563     * @return boolean                      status of the operation
564     * @throws StatusException              could throw specific SYNC_MOVEITEMSSTATUS_* exceptions
565     */
566    public function MoveMessage($folderid, $id, $newfolderid) {
567        return false;
568    }
569
570
571    /**----------------------------------------------------------------------------------------------------------
572     * private maildir-specific internals
573     */
574
575    /**
576     * Searches for the message
577     *
578     * @param string        $id        id of the message
579     *
580     * @access private
581     * @return string
582     */
583    private function findMessage($id) {
584        // We could use 'this->folderid' for path info but we currently
585        // only support a single INBOX. We also have to use a glob '*'
586        // because we don't know the flags of the message we're looking for.
587
588        $dirname = $this->getPath();
589        $dir = opendir($dirname);
590
591        while($entry = readdir($dir)) {
592            if(strpos($entry,$id) === 0)
593                return $entry;
594        }
595        return false; // not found
596    }
597
598    /**
599     * Parses the message and return only the plaintext body
600     *
601     * @param string        $message        html message
602     *
603     * @access private
604     * @return string       plaintext message
605     */
606    private function getBody($message) {
607        $body = "";
608        $htmlbody = "";
609
610        $this->getBodyRecursive($message, "plain", $body);
611
612        if(!isset($body) || $body === "") {
613            $this->getBodyRecursive($message, "html", $body);
614            // remove css-style tags
615            $body = preg_replace("/<style.*?<\/style>/is", "", $body);
616            // remove all other html
617            $body = strip_tags($body);
618        }
619
620        return $body;
621    }
622
623    /**
624     * Get all parts in the message with specified type and concatenate them together, unless the
625     * Content-Disposition is 'attachment', in which case the text is apparently an attachment
626     *
627     * @param string        $message        mimedecode message(part)
628     * @param string        $message        message subtype
629     * @param string        &$body          body reference
630     *
631     * @access private
632     * @return
633     */
634    private function getBodyRecursive($message, $subtype, &$body) {
635        if(!isset($message->ctype_primary)) return;
636        if(strcasecmp($message->ctype_primary,"text")==0 && strcasecmp($message->ctype_secondary,$subtype)==0 && isset($message->body))
637            $body .= $message->body;
638
639        if(strcasecmp($message->ctype_primary,"multipart")==0 && isset($message->parts) && is_array($message->parts)) {
640            foreach($message->parts as $part) {
641                if(!isset($part->disposition) || strcasecmp($part->disposition,"attachment"))  {
642                    $this->getBodyRecursive($part, $subtype, $body);
643                }
644            }
645        }
646    }
647
648    /**
649     * Parses the received date
650     *
651     * @param string        $received        received date string
652     *
653     * @access private
654     * @return long
655     */
656    private function parseReceivedDate($received) {
657        $pos = strpos($received, ";");
658        if(!$pos)
659            return false;
660
661        $datestr = substr($received, $pos+1);
662        $datestr = ltrim($datestr);
663
664        return strtotime($datestr);
665    }
666
667    /**
668     * Moves everything in Maildir/new/* to Maildir/cur/
669     *
670     * @access private
671     * @return
672     */
673    private function moveNewToCur() {
674        $newdirname = MAILDIR_BASE . "/" . $this->store . "/" . MAILDIR_SUBDIR . "/new";
675
676        $newdir = opendir($newdirname);
677
678        while($newentry = readdir($newdir)) {
679            if($newentry{0} == ".")
680                continue;
681
682            // link/unlink == move. This is the way to move the message according to cr.yp.to
683            link($newdirname . "/" . $newentry, $this->getPath() . "/" . $newentry . ":2,");
684            unlink($newdirname . "/" . $newentry);
685        }
686    }
687
688    /**
689     * The path we're working on
690     *
691     * @access private
692     * @return string
693     */
694    private function getPath() {
695        return MAILDIR_BASE . "/" . $this->store . "/" . MAILDIR_SUBDIR . "/cur";
696    }
697}
698
699?>
Note: See TracBrowser for help on using the repository browser.