source: trunk/zpush/backend/expresso/providers/imapProvider.php @ 7589

Revision 7589, 66.5 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      :   imap.php
4 * Project   :   Z-Push
5 * Descr     :   This backend is based on
6 *               'BackendDiff' and implements an
7 *               IMAP interface
8 *
9 * Created   :   10.10.2007
10 *
11 * Copyright 2007 - 2012 Zarafa Deutschland GmbH
12 *
13 * This program is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU Affero General Public License, version 3,
15 * as published by the Free Software Foundation with the following additional
16 * term according to sec. 7:
17 *
18 * According to sec. 7 of the GNU Affero General Public License, version 3,
19 * the terms of the AGPL are supplemented with the following terms:
20 *
21 * "Zarafa" is a registered trademark of Zarafa B.V.
22 * "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
23 * The licensing of the Program under the AGPL does not imply a trademark license.
24 * Therefore any rights, title and interest in our trademarks remain entirely with us.
25 *
26 * However, if you propagate an unmodified version of the Program you are
27 * allowed to use the term "Z-Push" to indicate that you distribute the Program.
28 * Furthermore you may use our trademarks where it is necessary to indicate
29 * the intended purpose of a product or service provided you use it in accordance
30 * with honest practices in industrial or commercial matters.
31 * If you want to propagate modified versions of the Program under the name "Z-Push",
32 * you may only do so if you have a written permission by Zarafa Deutschland GmbH
33 * (to acquire a permission please contact Zarafa at trademark@zarafa.com).
34 *
35 * This program is distributed in the hope that it will be useful,
36 * but WITHOUT ANY WARRANTY; without even the implied warranty of
37 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38 * GNU Affero General Public License for more details.
39 *
40 * You should have received a copy of the GNU Affero General Public License
41 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
42 *
43 * Consult LICENSE file for details
44 ************************************************/
45
46include_once(__DIR__.'/../../../lib/default/diffbackend/diffbackend.php');
47include_once(__DIR__.'/../../../include/mimeDecode.php');
48require_once(__DIR__.'/../../../include/z_RFC822.php');
49
50
51class ExpressoImapProvider extends BackendDiff {
52    protected $wasteID;
53    protected $sentID;
54    protected $server;
55    protected $mbox;
56    protected $mboxFolder;
57    protected $username;
58    protected $domain;
59    protected $serverdelimiter;
60    protected $sinkfolders;
61    protected $sinkstates;
62
63    /**----------------------------------------------------------------------------------------------------------
64     * default backend methods
65     */
66
67    /**
68     * Authenticates the user
69     *
70     * @param string        $username
71     * @param string        $domain
72     * @param string        $password
73     *
74     * @access public
75     * @return boolean
76     * @throws FatalException   if php-imap module can not be found
77     */
78    public function Logon($username, $domain, $password) {
79        $this->wasteID = false;
80        $this->sentID = false;
81        $this->server = "{" . IMAP_SERVER . ":" . IMAP_PORT . "/imap" . IMAP_OPTIONS . "}";
82
83        if (!function_exists("imap_open"))
84            throw new FatalException("ExpressoImapProvider(): php-imap module is not installed", 0, null, LOGLEVEL_FATAL);
85
86        // open the IMAP-mailbox
87        $this->mbox = @imap_open($this->server , $username, $password, OP_HALFOPEN);
88        $this->mboxFolder = "";
89
90        if ($this->mbox) {
91            ZLog::Write(LOGLEVEL_INFO, sprintf("ExpressoImapProvider->Logon(): User '%s' is authenticated on IMAP",$username));
92            $this->username = $username;
93            $this->domain = $domain;
94            // set serverdelimiter
95            $this->serverdelimiter = $this->getServerDelimiter();
96            return true;
97        }
98        else {
99            ZLog::Write(LOGLEVEL_ERROR, "ExpressoImapProvider->Logon(): can't connect: " . imap_last_error());
100            return false;
101        }
102    }
103
104    /**
105     * Logs off
106     * Called before shutting down the request to close the IMAP connection
107     * writes errors to the log
108     *
109     * @access public
110     * @return boolean
111     */
112    public function Logoff() {
113        if ($this->mbox) {
114            // list all errors
115            $errors = imap_errors();
116            if (is_array($errors)) {
117                foreach ($errors as $e)
118                    if (stripos($e, "fail") !== false)
119                        $level = LOGLEVEL_WARN;
120                    else
121                        $level = LOGLEVEL_DEBUG;
122
123                ZLog::Write($level, "ExpressoImapProvider->Logoff(): IMAP said: " . $e);
124            }
125            @imap_close($this->mbox);
126            ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->Logoff(): IMAP connection closed");
127        }
128        $this->SaveStorages();
129    }
130
131    /**
132     * Sends an e-mail
133     * This messages needs to be saved into the 'sent items' folder
134     *
135     * @param SyncSendMail  $sm     SyncSendMail object
136     *
137     * @access public
138     * @return boolean
139     * @throws StatusException
140     */
141    // TODO implement , $saveInSent = true
142    public function SendMail($sm) {
143        $forward = $reply = (isset($sm->source->itemid) && $sm->source->itemid) ? $sm->source->itemid : false;
144        $parent = false;
145
146        ZLog::Write(LOGLEVEL_DEBUG, sprintf("IMAPBackend->SendMail(): RFC822: %d bytes  forward-id: '%s' reply-id: '%s' parent-id: '%s' SaveInSent: '%s' ReplaceMIME: '%s'",
147            strlen($sm->mime), Utils::PrintAsString($sm->forwardflag), Utils::PrintAsString($sm->replyflag),
148            Utils::PrintAsString((isset($sm->source->folderid) ? $sm->source->folderid : false)),
149            Utils::PrintAsString(($sm->saveinsent)), Utils::PrintAsString(isset($sm->replacemime)) ));
150
151        if (isset($sm->source->folderid) && $sm->source->folderid)
152            // convert parent folder id back to work on an imap-id
153            $parent = $this->getImapIdFromFolderId($sm->source->folderid);
154
155
156        // by splitting the message in several lines we can easily grep later
157        foreach(preg_split("/((\r)?\n)/", $sm->mime) as $rfc822line)
158            ZLog::Write(LOGLEVEL_WBXML, "RFC822: ". $rfc822line);
159
160        $mobj = new Mail_mimeDecode($sm->mime);
161        $message = $mobj->decode(array('decode_headers' => false, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8'));
162
163        $Mail_RFC822 = new Mail_RFC822();
164        $toaddr = $ccaddr = $bccaddr = "";
165        if(isset($message->headers["to"]))
166            $toaddr = $this->parseAddr($Mail_RFC822->parseAddressList($message->headers["to"]));
167        if(isset($message->headers["cc"]))
168            $ccaddr = $this->parseAddr($Mail_RFC822->parseAddressList($message->headers["cc"]));
169        if(isset($message->headers["bcc"]))
170            $bccaddr = $this->parseAddr($Mail_RFC822->parseAddressList($message->headers["bcc"]));
171
172        // save some headers when forwarding mails (content type & transfer-encoding)
173        $headers = "";
174        $forward_h_ct = "";
175        $forward_h_cte = "";
176        $envelopefrom = "";
177
178        $use_orgbody = false;
179
180        // clean up the transmitted headers
181        // remove default headers because we are using imap_mail
182        $changedfrom = false;
183        $returnPathSet = false;
184        $body_base64 = false;
185        $org_charset = "";
186        $org_boundary = false;
187        $multipartmixed = false;
188        foreach($message->headers as $k => $v) {
189            if ($k == "subject" || $k == "to" || $k == "cc" || $k == "bcc")
190                continue;
191
192            if ($k == "content-type") {
193                // if the message is a multipart message, then we should use the sent body
194                if (preg_match("/multipart/i", $v)) {
195                    $use_orgbody = true;
196                    $org_boundary = $message->ctype_parameters["boundary"];
197                }
198
199                // save the original content-type header for the body part when forwarding
200                if ($sm->forwardflag && !$use_orgbody) {
201                    $forward_h_ct = $v;
202                    continue;
203                }
204
205                // set charset always to utf-8
206                $org_charset = $v;
207                $v = preg_replace("/charset=([A-Za-z0-9-\"']+)/", "charset=\"utf-8\"", $v);
208            }
209
210            if ($k == "content-transfer-encoding") {
211                // if the content was base64 encoded, encode the body again when sending
212                if (trim($v) == "base64") $body_base64 = true;
213
214                // save the original encoding header for the body part when forwarding
215                if ($sm->forwardflag) {
216                    $forward_h_cte = $v;
217                    continue;
218                }
219            }
220
221            // check if "from"-header is set, do nothing if it's set
222            // else set it to IMAP_DEFAULTFROM
223            if ($k == "from") {
224                if (trim($v)) {
225                    $changedfrom = true;
226                } elseif (! trim($v) && IMAP_DEFAULTFROM) {
227                    $changedfrom = true;
228                    if      (IMAP_DEFAULTFROM == 'username') $v = $this->username;
229                    else if (IMAP_DEFAULTFROM == 'domain')   $v = $this->domain;
230                    else $v = $this->username . IMAP_DEFAULTFROM;
231                    $envelopefrom = "-f$v";
232                }
233            }
234
235            // check if "Return-Path"-header is set
236            if ($k == "return-path") {
237                $returnPathSet = true;
238                if (! trim($v) && IMAP_DEFAULTFROM) {
239                    if      (IMAP_DEFAULTFROM == 'username') $v = $this->username;
240                    else if (IMAP_DEFAULTFROM == 'domain')   $v = $this->domain;
241                    else $v = $this->username . IMAP_DEFAULTFROM;
242                }
243            }
244
245            // all other headers stay
246            if ($headers) $headers .= "\n";
247            $headers .= ucfirst($k) . ": ". $v;
248        }
249
250        // set "From" header if not set on the device
251        if(IMAP_DEFAULTFROM && !$changedfrom){
252            if      (IMAP_DEFAULTFROM == 'username') $v = $this->username;
253            else if (IMAP_DEFAULTFROM == 'domain')   $v = $this->domain;
254            else $v = $this->username . IMAP_DEFAULTFROM;
255            if ($headers) $headers .= "\n";
256            $headers .= 'From: '.$v;
257            $envelopefrom = "-f$v";
258        }
259
260        // set "Return-Path" header if not set on the device
261        if(IMAP_DEFAULTFROM && !$returnPathSet){
262            if      (IMAP_DEFAULTFROM == 'username') $v = $this->username;
263            else if (IMAP_DEFAULTFROM == 'domain')   $v = $this->domain;
264            else $v = $this->username . IMAP_DEFAULTFROM;
265            if ($headers) $headers .= "\n";
266            $headers .= 'Return-Path: '.$v;
267        }
268
269        // if this is a multipart message with a boundary, we must use the original body
270        if ($use_orgbody) {
271            list(,$body) = $mobj->_splitBodyHeader($sm->mime);
272            $repl_body = $this->getBody($message);
273        }
274        else
275            $body = $this->getBody($message);
276
277        // reply
278        if ($sm->replyflag && $parent) {
279            $this->imap_reopenFolder($parent);
280            // receive entire mail (header + body) to decode body correctly
281            $origmail = @imap_fetchheader($this->mbox, $reply, FT_UID) . @imap_body($this->mbox, $reply, FT_PEEK | FT_UID);
282            if (!$origmail)
283                throw new StatusException(sprintf("ExpressoImapProvider->SendMail(): Could not open message id '%s' in folder id '%s' to be replied: %s", $reply, $parent, imap_last_error()), SYNC_COMMONSTATUS_ITEMNOTFOUND);
284
285            $mobj2 = new Mail_mimeDecode($origmail);
286            // receive only body
287            $body .= $this->getBody($mobj2->decode(array('decode_headers' => false, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8')));
288            // unset mimedecoder & origmail - free memory
289            unset($mobj2);
290            unset($origmail);
291        }
292
293        // encode the body to base64 if it was sent originally in base64 by the pda
294        // contrib - chunk base64 encoded body
295        if ($body_base64 && !$sm->forwardflag) $body = chunk_split(base64_encode($body));
296
297
298        // forward
299        if ($sm->forwardflag && $parent) {
300            $this->imap_reopenFolder($parent);
301            // receive entire mail (header + body)
302            $origmail = @imap_fetchheader($this->mbox, $forward, FT_UID) . @imap_body($this->mbox, $forward, FT_PEEK | FT_UID);
303
304            if (!$origmail)
305                throw new StatusException(sprintf("ExpressoImapProvider->SendMail(): Could not open message id '%s' in folder id '%s' to be forwarded: %s", $forward, $parent, imap_last_error()), SYNC_COMMONSTATUS_ITEMNOTFOUND);
306
307            if (!defined('IMAP_INLINE_FORWARD') || IMAP_INLINE_FORWARD === false) {
308                // contrib - chunk base64 encoded body
309                if ($body_base64) $body = chunk_split(base64_encode($body));
310                //use original boundary if it's set
311                $boundary = ($org_boundary) ? $org_boundary : false;
312                // build a new mime message, forward entire old mail as file
313                list($aheader, $body) = $this->mail_attach("forwarded_message.eml",strlen($origmail),$origmail, $body, $forward_h_ct, $forward_h_cte,$boundary);
314                // add boundary headers
315                $headers .= "\n" . $aheader;
316
317            }
318            else {
319                $mobj2 = new Mail_mimeDecode($origmail);
320                $mess2 = $mobj2->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8'));
321
322                if (!$use_orgbody)
323                    $nbody = $body;
324                else
325                    $nbody = $repl_body;
326
327                $nbody .= "\r\n\r\n";
328                $nbody .= "-----Original Message-----\r\n";
329                if(isset($mess2->headers['from']))
330                    $nbody .= "From: " . $mess2->headers['from'] . "\r\n";
331                if(isset($mess2->headers['to']) && strlen($mess2->headers['to']) > 0)
332                    $nbody .= "To: " . $mess2->headers['to'] . "\r\n";
333                if(isset($mess2->headers['cc']) && strlen($mess2->headers['cc']) > 0)
334                    $nbody .= "Cc: " . $mess2->headers['cc'] . "\r\n";
335                if(isset($mess2->headers['date']))
336                    $nbody .= "Sent: " . $mess2->headers['date'] . "\r\n";
337                if(isset($mess2->headers['subject']))
338                    $nbody .= "Subject: " . $mess2->headers['subject'] . "\r\n";
339                $nbody .= "\r\n";
340                $nbody .= $this->getBody($mess2);
341
342                if ($body_base64) {
343                    // contrib - chunk base64 encoded body
344                    $nbody = chunk_split(base64_encode($nbody));
345                    if ($use_orgbody)
346                        // contrib - chunk base64 encoded body
347                        $repl_body = chunk_split(base64_encode($repl_body));
348                }
349
350                if ($use_orgbody) {
351                    ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): -------------------");
352                    ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): old:\n'$repl_body'\nnew:\n'$nbody'\nund der body:\n'$body'");
353                    //$body is quoted-printable encoded while $repl_body and $nbody are plain text,
354                    //so we need to decode $body in order replace to take place
355                    $body = str_replace($repl_body, $nbody, quoted_printable_decode($body));
356                }
357                else
358                    $body = $nbody;
359
360
361                if(isset($mess2->parts)) {
362                    $attached = false;
363
364                    if ($org_boundary) {
365                        $att_boundary = $org_boundary;
366                        // cut end boundary from body
367                        $body = substr($body, 0, strrpos($body, "--$att_boundary--"));
368                    }
369                    else {
370                        $att_boundary = strtoupper(md5(uniqid(time())));
371                        // add boundary headers
372                        $headers .= "\n" . "Content-Type: multipart/mixed; boundary=$att_boundary";
373                        $multipartmixed = true;
374                    }
375
376                    foreach($mess2->parts as $part) {
377                        if(isset($part->disposition) && ($part->disposition == "attachment" || $part->disposition == "inline")) {
378
379                            if(isset($part->d_parameters['filename']))
380                                $attname = $part->d_parameters['filename'];
381                            else if(isset($part->ctype_parameters['name']))
382                                $attname = $part->ctype_parameters['name'];
383                            else if(isset($part->headers['content-description']))
384                                $attname = $part->headers['content-description'];
385                            else $attname = "unknown attachment";
386
387                            // ignore html content
388                            if ($part->ctype_primary == "text" && $part->ctype_secondary == "html") {
389                                continue;
390                            }
391                            //
392                            if ($use_orgbody || $attached) {
393                                $body .= $this->enc_attach_file($att_boundary, $attname, strlen($part->body),$part->body, $part->ctype_primary ."/". $part->ctype_secondary);
394                            }
395                            // first attachment
396                            else {
397                                $encmail = $body;
398                                $attached = true;
399                                $body = $this->enc_multipart($att_boundary, $body, $forward_h_ct, $forward_h_cte);
400                                $body .= $this->enc_attach_file($att_boundary, $attname, strlen($part->body),$part->body, $part->ctype_primary ."/". $part->ctype_secondary);
401                            }
402                        }
403                    }
404                    if ($multipartmixed && strpos(strtolower($mess2->headers['content-type']), "alternative") !== false) {
405                        //this happens if a multipart/alternative message is forwarded
406                        //then it's a multipart/mixed message which consists of:
407                        //1. text/plain part which was written on the mobile
408                        //2. multipart/alternative part which is the original message
409                        $body = "This is a message with multiple parts in MIME format.\n--".
410                            $att_boundary.
411                            "\nContent-Type: $forward_h_ct\nContent-Transfer-Encoding: $forward_h_cte\n\n".
412                            (($body_base64) ? chunk_split(base64_encode($message->body)) : rtrim($message->body)).
413                            "\n--".$att_boundary.
414                            "\nContent-Type: {$mess2->headers['content-type']}\n\n".
415                            @imap_body($this->mbox, $forward, FT_PEEK | FT_UID)."\n\n";
416                    }
417                    $body .= "--$att_boundary--\n\n";
418                }
419
420                unset($mobj2);
421            }
422
423            // unset origmail - free memory
424            unset($origmail);
425
426        }
427
428        // remove carriage-returns from body
429        $body = str_replace("\r\n", "\n", $body);
430
431        if (!$multipartmixed) {
432            if (!empty($forward_h_ct)) $headers .= "\nContent-Type: $forward_h_ct";
433            if (!empty($forward_h_cte)) $headers .= "\nContent-Transfer-Encoding: $forward_h_cte";
434            //  if body was quoted-printable, convert it again
435            if (isset($message->headers["content-transfer-encoding"]) && strtolower($message->headers["content-transfer-encoding"]) == "quoted-printable") {
436                $body = quoted_printable_encode($body);
437            }
438        }
439
440        // more debugging
441        ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): parsed message: ". print_r($message,1));
442        ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): headers: $headers");
443        ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): subject: {$message->headers["subject"]}");
444        ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): body: $body");
445
446        if (!defined('IMAP_USE_IMAPMAIL') || IMAP_USE_IMAPMAIL == true) {
447            $send =  @imap_mail ( $toaddr, $message->headers["subject"], $body, $headers, $ccaddr, $bccaddr);
448        }
449        else {
450            if (!empty($ccaddr))  $headers .= "\nCc: $ccaddr";
451            if (!empty($bccaddr)) $headers .= "\nBcc: $bccaddr";
452            $send =  @mail ( $toaddr, $message->headers["subject"], $body, $headers, $envelopefrom );
453        }
454
455        // email sent?
456        if (!$send)
457            throw new StatusException(sprintf("ExpressoImapProvider->SendMail(): The email could not be sent. Last IMAP-error: %s", imap_last_error()), SYNC_COMMONSTATUS_MAILSUBMISSIONFAILED);
458
459        // add message to the sent folder
460        // build complete headers
461        $headers .= "\nTo: $toaddr";
462        $headers .= "\nSubject: " . $message->headers["subject"];
463
464        if (!defined('IMAP_USE_IMAPMAIL') || IMAP_USE_IMAPMAIL == true) {
465            if (!empty($ccaddr))  $headers .= "\nCc: $ccaddr";
466            if (!empty($bccaddr)) $headers .= "\nBcc: $bccaddr";
467        }
468        ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): complete headers: $headers");
469
470        $asf = false;
471        if ($this->sentID) {
472            $asf = $this->addSentMessage($this->sentID, $headers, $body);
473        }
474        else if (IMAP_SENTFOLDER) {
475            $asf = $this->addSentMessage(IMAP_SENTFOLDER, $headers, $body);
476            ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->SendMail(): Outgoing mail saved in configured 'Sent' folder '%s': %s", IMAP_SENTFOLDER, Utils::PrintAsString($asf)));
477        }
478        // No Sent folder set, try defaults
479        else {
480            ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): No Sent mailbox set");
481            if($this->addSentMessage("INBOX.Sent", $headers, $body)) {
482                ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): Outgoing mail saved in 'INBOX.Sent'");
483                $asf = true;
484            }
485            else if ($this->addSentMessage("Sent", $headers, $body)) {
486                ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail(): Outgoing mail saved in 'Sent'");
487                $asf = true;
488            }
489            else if ($this->addSentMessage("Sent Items", $headers, $body)) {
490                ZLog::Write(LOGLEVEL_DEBUG, "ExpressoImapProvider->SendMail():IMAP-SendMail: Outgoing mail saved in 'Sent Items'");
491                $asf = true;
492            }
493        }
494
495        if (!$asf) {
496            ZLog::Write(LOGLEVEL_ERROR, "ExpressoImapProvider->SendMail(): The email could not be saved to Sent Items folder. Check your configuration.");
497        }
498
499        return $send;
500    }
501
502    /**
503     * Returns the waste basket
504     *
505     * @access public
506     * @return string
507     */
508    public function GetWasteBasket() {
509        // TODO this could be retrieved from the DeviceFolderCache
510        if ($this->wasteID == false) {
511            //try to get the waste basket without doing complete hierarchy sync
512            $wastebaskt = @imap_getmailboxes($this->mbox, $this->server, "Trash");
513            if (isset($wastebaskt[0])) {
514                $this->wasteID = $this->convertImapId(substr($wastebaskt[0]->name, strlen($this->server)));
515                return $this->wasteID;
516            }
517            //try get waste id from hierarchy if it wasn't possible with above for some reason
518            $this->GetHierarchy();
519        }
520        return $this->wasteID;
521    }
522
523    /**
524     * Returns the content of the named attachment as stream. The passed attachment identifier is
525     * the exact string that is returned in the 'AttName' property of an SyncAttachment.
526     * Any information necessary to find the attachment must be encoded in that 'attname' property.
527     * Data is written directly (with print $data;)
528     *
529     * @param string        $attname
530     *
531     * @access public
532     * @return SyncItemOperationsAttachment
533     * @throws StatusException
534     */
535    public function GetAttachmentData($attname) {
536        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->GetAttachmentData('%s')", $attname));
537
538        list($folderid, $id, $part) = explode(":", $attname);
539
540        if (!$folderid || !$id || !$part)
541            throw new StatusException(sprintf("ExpressoImapProvider->GetAttachmentData('%s'): Error, attachment name key can not be parsed", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
542
543        // convert back to work on an imap-id
544        $folderImapid = $this->getImapIdFromFolderId($folderid);
545
546        $this->imap_reopenFolder($folderImapid);
547        $mail = @imap_fetchheader($this->mbox, $id, FT_UID) . @imap_body($this->mbox, $id, FT_PEEK | FT_UID);
548
549        $mobj = new Mail_mimeDecode($mail);
550        $message = $mobj->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8'));
551
552        if (!isset($message->parts[$part]->body))
553            throw new StatusException(sprintf("ExpressoImapProvider->GetAttachmentData('%s'): Error, requested part key can not be found: '%d'", $attname, $part), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT);
554
555        // unset mimedecoder & mail
556        unset($mobj);
557        unset($mail);
558
559        include_once('include/stringstreamwrapper.php');
560        $attachment = new SyncItemOperationsAttachment();
561        $attachment->data = StringStreamWrapper::Open($message->parts[$part]->body);
562        if (isset($message->parts[$part]->ctype_primary) && isset($message->parts[$part]->ctype_secondary))
563            $attachment->contenttype = $message->parts[$part]->ctype_primary .'/'.$message->parts[$part]->ctype_secondary;
564
565        return $attachment;
566    }
567
568    /**
569     * Indicates if the backend has a ChangesSink.
570     * A sink is an active notification mechanism which does not need polling.
571     * The IMAP backend simulates a sink by polling status information of the folder
572     *
573     * @access public
574     * @return boolean
575     */
576    public function HasChangesSink() {
577        $this->sinkfolders = array();
578        $this->sinkstates = array();
579        return true;
580    }
581
582    /**
583     * The folder should be considered by the sink.
584     * Folders which were not initialized should not result in a notification
585     * of IBacken->ChangesSink().
586     *
587     * @param string        $folderid
588     *
589     * @access public
590     * @return boolean      false if found can not be found
591     */
592    public function ChangesSinkInitialize($folderid) {
593        ZLog::Write(LOGLEVEL_DEBUG, sprintf("IMAPBackend->ChangesSinkInitialize(): folderid '%s'", $folderid));
594
595        $imapid = $this->getImapIdFromFolderId($folderid);
596
597        if ($imapid) {
598            $this->sinkfolders[] = $imapid;
599            return true;
600        }
601
602        return false;
603    }
604
605    /**
606     * The actual ChangesSink.
607     * For max. the $timeout value this method should block and if no changes
608     * are available return an empty array.
609     * If changes are available a list of folderids is expected.
610     *
611     * @param int           $timeout        max. amount of seconds to block
612     *
613     * @access public
614     * @return array
615     */
616    public function ChangesSink($timeout = 30) {
617        $notifications = array();
618        $stopat = time() + $timeout - 1;
619
620        while($stopat > time() && empty($notifications)) {
621            foreach ($this->sinkfolders as $imapid) {
622                $this->imap_reopenFolder($imapid);
623
624                // courier-imap only cleares the status cache after checking
625                @imap_check($this->mbox);
626
627                $status = @imap_status($this->mbox, $this->server . $imapid, SA_ALL);
628                if (!$status) {
629                    ZLog::Write(LOGLEVEL_WARN, sprintf("ChangesSink: could not stat folder '%s': %s ", $this->getFolderIdFromImapId($imapid), imap_last_error()));
630                }
631                else {
632                    $newstate = "M:". $status->messages ."-R:". $status->recent ."-U:". $status->unseen;
633
634                    if (! isset($this->sinkstates[$imapid]) )
635                        $this->sinkstates[$imapid] = $newstate;
636
637                    if ($this->sinkstates[$imapid] != $newstate) {
638                        $notifications[] = $this->getFolderIdFromImapId($imapid);
639                        $this->sinkstates[$imapid] = $newstate;
640                    }
641                }
642            }
643
644            if (empty($notifications))
645                sleep(5);
646        }
647
648        return $notifications;
649    }
650
651
652    /**----------------------------------------------------------------------------------------------------------
653     * implemented DiffBackend methods
654     */
655
656
657    /**
658     * Returns a list (array) of folders.
659     *
660     * @access public
661     * @return array/boolean        false if the list could not be retrieved
662     */
663    public function GetFolderList() {
664        $folders = array();
665
666        $list = @imap_getmailboxes($this->mbox, $this->server, "*");
667        if (is_array($list)) {
668            // reverse list to obtain folders in right order
669            $list = array_reverse($list);
670
671            foreach ($list as $val) {
672                $box = array();
673                // cut off serverstring
674                $imapid = substr($val->name, strlen($this->server));
675                $box["id"] = $this->convertImapId($imapid);
676
677                $fhir = explode($val->delimiter, $imapid);
678                if (count($fhir) > 1) {
679                    $this->getModAndParentNames($fhir, $box["mod"], $imapparent);
680                    $box["parent"] = $this->convertImapId($imapparent);
681                }
682                else {
683                    $box["mod"] = $imapid;
684                    $box["parent"] = "0";
685                }
686                $folders[]=$box;
687            }
688        }
689        else {
690            ZLog::Write(LOGLEVEL_WARN, "ExpressoImapProvider->GetFolderList(): imap_list failed: " . imap_last_error());
691            return false;
692        }
693
694        return $folders;
695    }
696
697    /**
698     * Returns an actual SyncFolder object
699     *
700     * @param string        $id           id of the folder
701     *
702     * @access public
703     * @return object       SyncFolder with information
704     */
705    public function GetFolder($id) {
706        $folder = new SyncFolder();
707        $folder->serverid = $id;
708
709        // convert back to work on an imap-id
710        $imapid = $this->getImapIdFromFolderId($id);
711
712        // explode hierarchy
713        $fhir = explode($this->serverdelimiter, $imapid);
714
715        // compare on lowercase strings
716        $lid = strtolower($imapid);
717// TODO WasteID or SentID could be saved for later ussage
718        if($lid == "inbox") {
719            $folder->parentid = "0"; // Root
720            $folder->displayname = "Inbox";
721            $folder->type = SYNC_FOLDER_TYPE_INBOX;
722        }
723        // Zarafa IMAP-Gateway outputs
724        else if($lid == "drafts") {
725            $folder->parentid = "0";
726            $folder->displayname = "Drafts";
727            $folder->type = SYNC_FOLDER_TYPE_DRAFTS;
728        }
729        else if($lid == "trash") {
730            $folder->parentid = "0";
731            $folder->displayname = "Trash";
732            $folder->type = SYNC_FOLDER_TYPE_WASTEBASKET;
733            $this->wasteID = $id;
734        }
735        else if($lid == "sent" || $lid == "sent items" || $lid == IMAP_SENTFOLDER) {
736            $folder->parentid = "0";
737            $folder->displayname = "Sent";
738            $folder->type = SYNC_FOLDER_TYPE_SENTMAIL;
739            $this->sentID = $id;
740        }
741        // courier-imap outputs and cyrus-imapd outputs
742        else if($lid == "inbox.drafts" || $lid == "inbox/drafts") {
743            $folder->parentid = $this->convertImapId($fhir[0]);
744            $folder->displayname = "Drafts";
745            $folder->type = SYNC_FOLDER_TYPE_DRAFTS;
746        }
747        else if($lid == "inbox.trash" || $lid == "inbox/trash") {
748            $folder->parentid = $this->convertImapId($fhir[0]);
749            $folder->displayname = "Trash";
750            $folder->type = SYNC_FOLDER_TYPE_WASTEBASKET;
751            $this->wasteID = $id;
752        }
753        else if($lid == "inbox.sent" || $lid == "inbox/sent") {
754            $folder->parentid = $this->convertImapId($fhir[0]);
755            $folder->displayname = "Sent";
756            $folder->type = SYNC_FOLDER_TYPE_SENTMAIL;
757            $this->sentID = $id;
758        }
759
760        // define the rest as other-folders
761        else {
762            if (count($fhir) > 1) {
763                $this->getModAndParentNames($fhir, $folder->displayname, $imapparent);
764                $folder->parentid = $this->convertImapId($imapparent);
765                $folder->displayname = Utils::Utf7_to_utf8(Utils::Utf7_iconv_decode($folder->displayname));
766            }
767            else {
768                $folder->displayname = Utils::Utf7_to_utf8(Utils::Utf7_iconv_decode($imapid));
769                $folder->parentid = "0";
770            }
771            $folder->type = SYNC_FOLDER_TYPE_OTHER;
772        }
773
774        //advanced debugging
775        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->GetFolder('%s'): '%s'", $id, $folder));
776
777        return $folder;
778    }
779
780    /**
781     * Returns folder stats. An associative array with properties is expected.
782     *
783     * @param string        $id             id of the folder
784     *
785     * @access public
786     * @return array
787     */
788    public function StatFolder($id) {
789        $folder = $this->GetFolder($id);
790
791        $stat = array();
792        $stat["id"] = $id;
793        $stat["parent"] = $folder->parentid;
794        $stat["mod"] = $folder->displayname;
795
796        return $stat;
797    }
798
799    /**
800     * Creates or modifies a folder
801     * The folder type is ignored in IMAP, as all folders are Email folders
802     *
803     * @param string        $folderid       id of the parent folder
804     * @param string        $oldid          if empty -> new folder created, else folder is to be renamed
805     * @param string        $displayname    new folder name (to be created, or to be renamed to)
806     * @param int           $type           folder type
807     *
808     * @access public
809     * @return boolean                      status
810     * @throws StatusException              could throw specific SYNC_FSSTATUS_* exceptions
811     *
812     */
813    public function ChangeFolder($folderid, $oldid, $displayname, $type){
814        ZLog::Write(LOGLEVEL_INFO, sprintf("ExpressoImapProvider->ChangeFolder('%s','%s','%s','%s')", $folderid, $oldid, $displayname, $type));
815
816        // go to parent mailbox
817        $this->imap_reopenFolder($folderid);
818
819        // build name for new mailboxBackendMaildir
820        $displayname = Utils::Utf7_iconv_encode(Utils::Utf8_to_utf7($displayname));
821        $newname = $this->server . $folderid . $this->serverdelimiter . $displayname;
822
823        $csts = false;
824        // if $id is set => rename mailbox, otherwise create
825        if ($oldid) {
826            // rename doesn't work properly with IMAP
827            // the activesync client doesn't support a 'changing ID'
828            // TODO this would be solved by implementing hex ids (Mantis #459)
829            //$csts = imap_renamemailbox($this->mbox, $this->server . imap_utf7_encode(str_replace(".", $this->serverdelimiter, $oldid)), $newname);
830        }
831        else {
832            $csts = @imap_createmailbox($this->mbox, $newname);
833        }
834        if ($csts) {
835            return $this->StatFolder($folderid . $this->serverdelimiter . $displayname);
836        }
837        else
838            return false;
839    }
840
841    /**
842     * Deletes a folder
843     *
844     * @param string        $id
845     * @param string        $parent         is normally false
846     *
847     * @access public
848     * @return boolean                      status - false if e.g. does not exist
849     * @throws StatusException              could throw specific SYNC_FSSTATUS_* exceptions
850     *
851     */
852    public function DeleteFolder($id, $parentid){
853        // TODO implement
854        return false;
855    }
856
857    /**
858     * Returns a list (array) of messages
859     *
860     * @param string        $folderid       id of the parent folder
861     * @param long          $cutoffdate     timestamp in the past from which on messages should be returned
862     *
863     * @access public
864     * @return array/false  array with messages or false if folder is not available
865     */
866    public function GetMessageList($folderid, $cutoffdate) {
867        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->GetMessageList('%s','%s')", $folderid, $cutoffdate));
868
869        $folderid = $this->getImapIdFromFolderId($folderid);
870
871        if ($folderid == false)
872            throw new StatusException("Folderid not found in cache", SYNC_STATUS_FOLDERHIERARCHYCHANGED);
873
874        $messages = array();
875        $this->imap_reopenFolder($folderid, true);
876
877        $sequence = "1:*";
878        if ($cutoffdate > 0) {
879            $search = @imap_search($this->mbox, "SINCE ". date("d-M-Y", $cutoffdate));
880            if ($search !== false)
881                $sequence = implode(",", $search);
882        }
883        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->GetMessageList(): searching with sequence '%s'", $sequence));
884        $overviews = @imap_fetch_overview($this->mbox, $sequence);
885
886        if (!$overviews || !is_array($overviews)) {
887            ZLog::Write(LOGLEVEL_WARN, sprintf("ExpressoImapProvider->GetMessageList('%s','%s'): Failed to retrieve overview: %s",$folderid, $cutoffdate, imap_last_error()));
888            return $messages;
889        }
890
891        foreach($overviews as $overview) {
892            $date = "";
893            $vars = get_object_vars($overview);
894            if (array_key_exists( "date", $vars)) {
895                // message is out of range for cutoffdate, ignore it
896                if ($this->cleanupDate($overview->date) < $cutoffdate) continue;
897                $date = $overview->date;
898            }
899
900            // cut of deleted messages
901            if (array_key_exists( "deleted", $vars) && $overview->deleted)
902                continue;
903
904            if (array_key_exists( "uid", $vars)) {
905                $message = array();
906                $message["mod"] = $date;
907                $message["id"] = $overview->uid;
908                // 'seen' aka 'read' is the only flag we want to know about
909                $message["flags"] = 0;
910
911                if(array_key_exists( "seen", $vars) && $overview->seen)
912                    $message["flags"] = 1;
913
914                array_push($messages, $message);
915            }
916        }
917        return $messages;
918    }
919
920    /**
921     * Returns the actual SyncXXX object type.
922     *
923     * @param string            $folderid           id of the parent folder
924     * @param string            $id                 id of the message
925     * @param ContentParameters $contentparameters  parameters of the requested message (truncation, mimesupport etc)
926     *
927     * @access public
928     * @return object/false     false if the message could not be retrieved
929     */
930    public function GetMessage($folderid, $id, $contentparameters) {
931        $truncsize = Utils::GetTruncSize($contentparameters->GetTruncation());
932        $mimesupport = $contentparameters->GetMimeSupport();
933        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->GetMessage('%s','%s')", $folderid,  $id));
934
935        $folderImapid = $this->getImapIdFromFolderId($folderid);
936
937        // Get flags, etc
938        $stat = $this->StatMessage($folderid, $id);
939
940        if ($stat) {
941            $this->imap_reopenFolder($folderImapid);
942            $mail = @imap_fetchheader($this->mbox, $id, FT_UID) . @imap_body($this->mbox, $id, FT_PEEK | FT_UID);
943
944            $mobj = new Mail_mimeDecode($mail);
945            $message = $mobj->decode(array('decode_headers' => true, 'decode_bodies' => true, 'include_bodies' => true, 'charset' => 'utf-8'));
946
947            $output = new SyncMail();
948
949            $body = $this->getBody($message);
950            $output->bodysize = strlen($body);
951
952            // truncate body, if requested
953            if(strlen($body) > $truncsize) {
954                $body = Utils::Utf8_truncate($body, $truncsize);
955                $output->bodytruncated = 1;
956            } else {
957                $body = $body;
958                $output->bodytruncated = 0;
959            }
960
961            $output->body = $body;
962            $output->datereceived = isset($message->headers["date"]) ? $this->cleanupDate($message->headers["date"]) : null;
963            $output->messageclass = "IPM.Note";
964            $output->subject = isset($message->headers["subject"]) ? $message->headers["subject"] : "";
965            $output->read = $stat["flags"];
966            $output->from = isset($message->headers["from"]) ? $message->headers["from"] : null;
967
968            $Mail_RFC822 = new Mail_RFC822();
969            $toaddr = $ccaddr = $replytoaddr = array();
970            if(isset($message->headers["to"]))
971                $toaddr = $Mail_RFC822->parseAddressList($message->headers["to"]);
972            if(isset($message->headers["cc"]))
973                $ccaddr = $Mail_RFC822->parseAddressList($message->headers["cc"]);
974            if(isset($message->headers["reply_to"]))
975                $replytoaddr = $Mail_RFC822->parseAddressList($message->headers["reply_to"]);
976
977            $output->to = array();
978            $output->cc = array();
979            $output->reply_to = array();
980            foreach(array("to" => $toaddr, "cc" => $ccaddr, "reply_to" => $replytoaddr) as $type => $addrlist) {
981                foreach($addrlist as $addr) {
982                    $address = $addr->mailbox . "@" . $addr->host;
983                    $name = $addr->personal;
984
985                    if (!isset($output->displayto) && $name != "")
986                        $output->displayto = $name;
987
988                    if($name == "" || $name == $address)
989                        $fulladdr = w2u($address);
990                    else {
991                        if (substr($name, 0, 1) != '"' && substr($name, -1) != '"') {
992                            $fulladdr = "\"" . w2u($name) ."\" <" . w2u($address) . ">";
993                        }
994                        else {
995                            $fulladdr = w2u($name) ." <" . w2u($address) . ">";
996                        }
997                    }
998
999                    array_push($output->$type, $fulladdr);
1000                }
1001            }
1002
1003            // convert mime-importance to AS-importance
1004            if (isset($message->headers["x-priority"])) {
1005                $mimeImportance =  preg_replace("/\D+/", "", $message->headers["x-priority"]);
1006                if ($mimeImportance > 3)
1007                    $output->importance = 0;
1008                if ($mimeImportance == 3)
1009                    $output->importance = 1;
1010                if ($mimeImportance < 3)
1011                    $output->importance = 2;
1012            }
1013
1014            // Attachments are only searched in the top-level part
1015            if(isset($message->parts)) {
1016                $mparts = $message->parts;
1017                for ($i=0; $i<count($mparts); $i++) {
1018                    $part = $mparts[$i];
1019                    //recursively add parts
1020                    if($part->ctype_primary == "multipart" && ($part->ctype_secondary == "mixed" || $part->ctype_secondary == "alternative"  || $part->ctype_secondary == "related")) {
1021                        foreach($part->parts as $spart)
1022                            $mparts[] = $spart;
1023                        continue;
1024                    }
1025                    //add part as attachment if it's disposition indicates so or if it is not a text part
1026                    if ((isset($part->disposition) && ($part->disposition == "attachment" || $part->disposition == "inline")) ||
1027                        (isset($part->ctype_primary) && $part->ctype_primary != "text")) {
1028
1029                        if (!isset($output->attachments) || !is_array($output->attachments))
1030                            $output->attachments = array();
1031
1032                        $attachment = new SyncAttachment();
1033
1034                        if (isset($part->body))
1035                            $attachment->attsize = strlen($part->body);
1036
1037                        if(isset($part->d_parameters['filename']))
1038                            $attname = $part->d_parameters['filename'];
1039                        else if(isset($part->ctype_parameters['name']))
1040                            $attname = $part->ctype_parameters['name'];
1041                        else if(isset($part->headers['content-description']))
1042                            $attname = $part->headers['content-description'];
1043                        else $attname = "unknown attachment";
1044
1045                        $attachment->displayname = $attname;
1046                        $attachment->attname = $folderid . ":" . $id . ":" . $i;
1047                        $attachment->attmethod = 1;
1048                        $attachment->attoid = isset($part->headers['content-id']) ? $part->headers['content-id'] : "";
1049                        array_push($output->attachments, $attachment);
1050                    }
1051
1052                }
1053            }
1054            // unset mimedecoder & mail
1055            unset($mobj);
1056            unset($mail);
1057
1058            $output->body .= ' '; //Adicionado pois por algum motivo o android corta o ultimo caractere ao fazer a operação maiores detalhes
1059            return $output;
1060
1061        }
1062
1063        return false;
1064    }
1065
1066    /**
1067     * Returns message stats, analogous to the folder stats from StatFolder().
1068     *
1069     * @param string        $folderid       id of the folder
1070     * @param string        $id             id of the message
1071     *
1072     * @access public
1073     * @return array/boolean
1074     */
1075    public function StatMessage($folderid, $id) {
1076        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->StatMessage('%s','%s')", $folderid,  $id));
1077        $folderImapid = $this->getImapIdFromFolderId($folderid);
1078
1079        $this->imap_reopenFolder($folderImapid);
1080        $overview = @imap_fetch_overview( $this->mbox , $id , FT_UID);
1081
1082        if (!$overview) {
1083            ZLog::Write(LOGLEVEL_WARN, sprintf("ExpressoImapProvider->StatMessage('%s','%s'): Failed to retrieve overview: %s", $folderid,  $id, imap_last_error()));
1084            return false;
1085        }
1086
1087        // check if variables for this overview object are available
1088        $vars = get_object_vars($overview[0]);
1089
1090        // without uid it's not a valid message
1091        if (! array_key_exists( "uid", $vars)) return false;
1092
1093        $entry = array();
1094        $entry["mod"] = (array_key_exists( "date", $vars)) ? $overview[0]->date : "";
1095        $entry["id"] = $overview[0]->uid;
1096        // 'seen' aka 'read' is the only flag we want to know about
1097        $entry["flags"] = 0;
1098
1099        if(array_key_exists( "seen", $vars) && $overview[0]->seen)
1100            $entry["flags"] = 1;
1101
1102        return $entry;
1103    }
1104
1105    /**
1106     * Called when a message has been changed on the mobile.
1107     * This functionality is not available for emails.
1108     *
1109     * @param string        $folderid       id of the folder
1110     * @param string        $id             id of the message
1111     * @param SyncXXX       $message        the SyncObject containing a message
1112     *
1113     * @access public
1114     * @return array                        same return value as StatMessage()
1115     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
1116     */
1117    public function ChangeMessage($folderid, $id, $message) {
1118        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->ChangeMessage('%s','%s','%s')", $folderid, $id, get_class($message)));
1119        // TODO recheck implementation
1120        // TODO this could throw several StatusExceptions like e.g. SYNC_STATUS_OBJECTNOTFOUND, SYNC_STATUS_SYNCCANNOTBECOMPLETED
1121        return false;
1122    }
1123
1124    /**
1125     * Changes the 'read' flag of a message on disk
1126     *
1127     * @param string        $folderid       id of the folder
1128     * @param string        $id             id of the message
1129     * @param int           $flags          read flag of the message
1130     *
1131     * @access public
1132     * @return boolean                      status of the operation
1133     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
1134     */
1135    public function SetReadFlag($folderid, $id, $flags) {
1136        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->SetReadFlag('%s','%s','%s')", $folderid, $id, $flags));
1137        $folderImapid = $this->getImapIdFromFolderId($folderid);
1138
1139        $this->imap_reopenFolder($folderImapid);
1140
1141        if ($flags == 0) {
1142            // set as "Unseen" (unread)
1143            $status = @imap_clearflag_full ( $this->mbox, $id, "\\Seen", ST_UID);
1144        } else {
1145            // set as "Seen" (read)
1146            $status = @imap_setflag_full($this->mbox, $id, "\\Seen",ST_UID);
1147        }
1148
1149        return $status;
1150    }
1151
1152    /**
1153     * Called when the user has requested to delete (really delete) a message
1154     *
1155     * @param string        $folderid       id of the folder
1156     * @param string        $id             id of the message
1157     *
1158     * @access public
1159     * @return boolean                      status of the operation
1160     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
1161     */
1162    public function DeleteMessage($folderid, $id) {
1163        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->DeleteMessage('%s','%s')", $folderid, $id));
1164        $folderImapid = $this->getImapIdFromFolderId($folderid);
1165
1166        $this->imap_reopenFolder($folderImapid);
1167        $s1 = @imap_delete ($this->mbox, $id, FT_UID);
1168        $s11 = @imap_setflag_full($this->mbox, $id, "\\Deleted", FT_UID);
1169        $s2 = @imap_expunge($this->mbox);
1170
1171        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->DeleteMessage('%s','%s'): result: s-delete: '%s' s-expunge: '%s' setflag: '%s'", $folderid, $id, $s1, $s2, $s11));
1172
1173        return ($s1 && $s2 && $s11);
1174    }
1175
1176    /**
1177     * Called when the user moves an item on the PDA from one folder to another
1178     *
1179     * @param string        $folderid       id of the source folder
1180     * @param string        $id             id of the message
1181     * @param string        $newfolderid    id of the destination folder
1182     *
1183     * @access public
1184     * @return boolean                      status of the operation
1185     * @throws StatusException              could throw specific SYNC_MOVEITEMSSTATUS_* exceptions
1186     */
1187    public function MoveMessage($folderid, $id, $newfolderid) {
1188        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->MoveMessage('%s','%s','%s')", $folderid, $id, $newfolderid));
1189        $folderImapid = $this->getImapIdFromFolderId($folderid);
1190        $newfolderImapid = $this->getImapIdFromFolderId($newfolderid);
1191
1192
1193        $this->imap_reopenFolder($folderImapid);
1194
1195        // TODO this should throw a StatusExceptions on errors like SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST,SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID,SYNC_MOVEITEMSSTATUS_CANNOTMOVE
1196
1197        // read message flags
1198        $overview = @imap_fetch_overview ( $this->mbox , $id, FT_UID);
1199
1200        if (!$overview)
1201            throw new StatusException(sprintf("ExpressoImapProvider->MoveMessage('%s','%s','%s'): Error, unable to retrieve overview of source message: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
1202        else {
1203            // get next UID for destination folder
1204            // when moving a message we have to announce through ActiveSync the new messageID in the
1205            // destination folder. This is a "guessing" mechanism as IMAP does not inform that value.
1206            // when lots of simultaneous operations happen in the destination folder this could fail.
1207            // in the worst case the moved message is displayed twice on the mobile.
1208            $destStatus = imap_status($this->mbox, $this->server . $newfolderImapid, SA_ALL);
1209            if (!$destStatus)
1210                throw new StatusException(sprintf("ExpressoImapProvider->MoveMessage('%s','%s','%s'): Error, unable to open destination folder: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
1211
1212            $newid = $destStatus->uidnext;
1213
1214            // move message
1215            $s1 = imap_mail_move($this->mbox, $id, $newfolderImapid, CP_UID);
1216            if (! $s1)
1217                throw new StatusException(sprintf("ExpressoImapProvider->MoveMessage('%s','%s','%s'): Error, copy to destination folder failed: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
1218
1219
1220            // delete message in from-folder
1221            $s2 = imap_expunge($this->mbox);
1222
1223            // open new folder
1224            $stat = $this->imap_reopenFolder($newfolderImapid);
1225            if (! $s1)
1226                throw new StatusException(sprintf("ExpressoImapProvider->MoveMessage('%s','%s','%s'): Error, openeing the destination folder: %s", $folderid, $id, $newfolderid, imap_last_error()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
1227
1228
1229            // remove all flags
1230            $s3 = @imap_clearflag_full ($this->mbox, $newid, "\\Seen \\Answered \\Flagged \\Deleted \\Draft", FT_UID);
1231            $newflags = "";
1232            if ($overview[0]->seen) $newflags .= "\\Seen";
1233            if ($overview[0]->flagged) $newflags .= " \\Flagged";
1234            if ($overview[0]->answered) $newflags .= " \\Answered";
1235            $s4 = @imap_setflag_full ($this->mbox, $newid, $newflags, FT_UID);
1236
1237            ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->MoveMessage('%s','%s','%s'): result s-move: '%s' s-expunge: '%s' unset-Flags: '%s' set-Flags: '%s'", $folderid, $id, $newfolderid, Utils::PrintAsString($s1), Utils::PrintAsString($s2), Utils::PrintAsString($s3), Utils::PrintAsString($s4)));
1238
1239            // return the new id "as string""
1240            return $newid . "";
1241        }
1242    }
1243
1244
1245    /**----------------------------------------------------------------------------------------------------------
1246     * protected IMAP methods
1247     */
1248
1249    /**
1250     * Unmasks a hex folderid and returns the imap folder id
1251     *
1252     * @param string        $folderid       hex folderid generated by convertImapId()
1253     *
1254     * @access protected
1255     * @return string       imap folder id
1256     */
1257    protected function getImapIdFromFolderId($folderid) {
1258        $this->InitializePermanentStorage();
1259
1260        if (isset($this->permanentStorage->fmFidFimap)) {
1261            if (isset($this->permanentStorage->fmFidFimap[$folderid])) {
1262                $imapId = $this->permanentStorage->fmFidFimap[$folderid];
1263                ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->getImapIdFromFolderId('%s') = %s", $folderid, $imapId));
1264                return $imapId;
1265            }
1266            else {
1267                ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->getImapIdFromFolderId('%s') = %s", $folderid, 'not found'));
1268                return false;
1269            }
1270        }
1271        ZLog::Write(LOGLEVEL_WARN, sprintf("ExpressoImapProvider->getImapIdFromFolderId('%s') = %s", $folderid, 'not initialized!'));
1272        return false;
1273    }
1274
1275    /**
1276     * Retrieves a hex folderid previousily masked imap
1277     *
1278     * @param string        $imapid         Imap folder id
1279     *
1280     * @access protected
1281     * @return string       hex folder id
1282     */
1283    protected function getFolderIdFromImapId($imapid) {
1284        $this->InitializePermanentStorage();
1285
1286        if (isset($this->permanentStorage->fmFimapFid)) {
1287            if (isset($this->permanentStorage->fmFimapFid[$imapid])) {
1288                $folderid = $this->permanentStorage->fmFimapFid[$imapid];
1289                ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->getFolderIdFromImapId('%s') = %s", $imapid, $folderid));
1290                return $folderid;
1291            }
1292            else {
1293                ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->getFolderIdFromImapId('%s') = %s", $imapid, 'not found'));
1294                return false;
1295            }
1296        }
1297        ZLog::Write(LOGLEVEL_WARN, sprintf("ExpressoImapProvider->getFolderIdFromImapId('%s') = %s", $imapid, 'not initialized!'));
1298        return false;
1299    }
1300
1301    /**
1302     * Masks a imap folder id into a generated hex folderid
1303     * The method getFolderIdFromImapId() is consulted so that an
1304     * imapid always returns the same hex folder id
1305     *
1306     * @param string        $imapid         Imap folder id
1307     *
1308     * @access protected
1309     * @return string       hex folder id
1310     */
1311    protected function convertImapId($imapid) {
1312        $this->InitializePermanentStorage();
1313
1314        // check if this imap id was converted before
1315        $folderid = $this->getFolderIdFromImapId($imapid);
1316
1317        // nothing found, so generate a new id and put it in the cache
1318        if (!$folderid) {
1319            // generate folderid and add it to the mapping
1320            $folderid = sprintf('%04x%04x', mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ));
1321
1322            // folderId to folderImap mapping
1323            if (!isset($this->permanentStorage->fmFidFimap))
1324                $this->permanentStorage->fmFidFimap = array();
1325
1326            $a = $this->permanentStorage->fmFidFimap;
1327            $a[$folderid] = $imapid;
1328            $this->permanentStorage->fmFidFimap = $a;
1329
1330            // folderImap to folderid mapping
1331            if (!isset($this->permanentStorage->fmFimapFid))
1332                $this->permanentStorage->fmFimapFid = array();
1333
1334            $b = $this->permanentStorage->fmFimapFid;
1335            $b[$imapid] = $folderid;
1336            $this->permanentStorage->fmFimapFid = $b;
1337        }
1338
1339        ZLog::Write(LOGLEVEL_DEBUG, sprintf("ExpressoImapProvider->convertImapId('%s') = %s", $imapid, $folderid));
1340
1341        return $folderid;
1342    }
1343
1344
1345    /**
1346     * Parses the message and return only the plaintext body
1347     *
1348     * @param string        $message        html message
1349     *
1350     * @access protected
1351     * @return string       plaintext message
1352     */
1353    protected function getBody($message) {
1354        $body = "";
1355        $htmlbody = "";
1356
1357        $this->getBodyRecursive($message, "plain", $body);
1358
1359        if($body === "") {
1360            $this->getBodyRecursive($message, "html", $body);
1361            // remove css-style tags
1362            $body = preg_replace("/<style.*?<\/style>/is", "", $body);
1363            // remove all other html
1364            $body = strip_tags($body);
1365        }
1366
1367        return $body;
1368    }
1369
1370    /**
1371     * Get all parts in the message with specified type and concatenate them together, unless the
1372     * Content-Disposition is 'attachment', in which case the text is apparently an attachment
1373     *
1374     * @param string        $message        mimedecode message(part)
1375     * @param string        $message        message subtype
1376     * @param string        &$body          body reference
1377     *
1378     * @access protected
1379     * @return
1380     */
1381    protected function getBodyRecursive($message, $subtype, &$body) {
1382        if(!isset($message->ctype_primary)) return;
1383        if(strcasecmp($message->ctype_primary,"text")==0 && strcasecmp($message->ctype_secondary,$subtype)==0 && isset($message->body))
1384            $body .= $message->body;
1385
1386        if(strcasecmp($message->ctype_primary,"multipart")==0 && isset($message->parts) && is_array($message->parts)) {
1387            foreach($message->parts as $part) {
1388                if(!isset($part->disposition) || strcasecmp($part->disposition,"attachment"))  {
1389                    $this->getBodyRecursive($part, $subtype, $body);
1390                }
1391            }
1392        }
1393    }
1394
1395    /**
1396     * Returns the serverdelimiter for folder parsing
1397     *
1398     * @access protected
1399     * @return string       delimiter
1400     */
1401    protected function getServerDelimiter() {
1402        $list = @imap_getmailboxes($this->mbox, $this->server, "*");
1403        if (is_array($list)) {
1404            $val = $list[0];
1405
1406            return $val->delimiter;
1407        }
1408        return "."; // default "."
1409    }
1410
1411    /**
1412     * Helper to re-initialize the folder to speed things up
1413     * Remember what folder is currently open and only change if necessary
1414     *
1415     * @param string        $folderid       id of the folder
1416     * @param boolean       $force          re-open the folder even if currently opened
1417     *
1418     * @access protected
1419     * @return
1420     */
1421    protected function imap_reopenFolder($folderid, $force = false) {
1422        // to see changes, the folder has to be reopened!
1423        if ($this->mboxFolder != $folderid || $force) {
1424            $s = @imap_reopen($this->mbox, $this->server . $folderid);
1425            // TODO throw status exception
1426            if (!$s) {
1427                ZLog::Write(LOGLEVEL_WARN, "ExpressoImapProvider->imap_reopenFolder('%s'): failed to change folder: ",$folderid, implode(", ", imap_errors()));
1428                return false;
1429            }
1430            $this->mboxFolder = $folderid;
1431        }
1432    }
1433
1434
1435    /**
1436     * Build a multipart RFC822, embedding body and one file (for attachments)
1437     *
1438     * @param string        $filenm         name of the file to be attached
1439     * @param long          $filesize       size of the file to be attached
1440     * @param string        $file_cont      content of the file
1441     * @param string        $body           current body
1442     * @param string        $body_ct        content-type
1443     * @param string        $body_cte       content-transfer-encoding
1444     * @param string        $boundary       optional existing boundary
1445     *
1446     * @access protected
1447     * @return array        with [0] => $mail_header and [1] => $mail_body
1448     */
1449    protected function mail_attach($filenm,$filesize,$file_cont,$body, $body_ct, $body_cte, $boundary = false) {
1450        if (!$boundary) $boundary = strtoupper(md5(uniqid(time())));
1451
1452        //remove the ending boundary because we will add it at the end
1453        $body = str_replace("--$boundary--", "", $body);
1454
1455        $mail_header = "Content-Type: multipart/mixed; boundary=$boundary\n";
1456
1457        // build main body with the sumitted type & encoding from the pda
1458        $mail_body  = $this->enc_multipart($boundary, $body, $body_ct, $body_cte);
1459        $mail_body .= $this->enc_attach_file($boundary, $filenm, $filesize, $file_cont);
1460
1461        $mail_body .= "--$boundary--\n\n";
1462        return array($mail_header, $mail_body);
1463    }
1464
1465    /**
1466     * Helper for mail_attach()
1467     *
1468     * @param string        $boundary       boundary
1469     * @param string        $body           current body
1470     * @param string        $body_ct        content-type
1471     * @param string        $body_cte       content-transfer-encoding
1472     *
1473     * @access protected
1474     * @return string       message body
1475     */
1476    protected function enc_multipart($boundary, $body, $body_ct, $body_cte) {
1477        $mail_body = "This is a multi-part message in MIME format\n\n";
1478        $mail_body .= "--$boundary\n";
1479        $mail_body .= "Content-Type: $body_ct\n";
1480        $mail_body .= "Content-Transfer-Encoding: $body_cte\n\n";
1481        $mail_body .= "$body\n\n";
1482
1483        return $mail_body;
1484    }
1485
1486    /**
1487     * Helper for mail_attach()
1488     *
1489     * @param string        $boundary       boundary
1490     * @param string        $filenm         name of the file to be attached
1491     * @param long          $filesize       size of the file to be attached
1492     * @param string        $file_cont      content of the file
1493     * @param string        $content_type   optional content-type
1494     *
1495     * @access protected
1496     * @return string       message body
1497     */
1498    protected function enc_attach_file($boundary, $filenm, $filesize, $file_cont, $content_type = "") {
1499        if (!$content_type) $content_type = "text/plain";
1500        $mail_body = "--$boundary\n";
1501        $mail_body .= "Content-Type: $content_type; name=\"$filenm\"\n";
1502        $mail_body .= "Content-Transfer-Encoding: base64\n";
1503        $mail_body .= "Content-Disposition: attachment; filename=\"$filenm\"\n";
1504        $mail_body .= "Content-Description: $filenm\n\n";
1505        //contrib - chunk base64 encoded attachments
1506        $mail_body .= chunk_split(base64_encode($file_cont)) . "\n\n";
1507
1508        return $mail_body;
1509    }
1510
1511    /**
1512     * Adds a message with seen flag to a specified folder (used for saving sent items)
1513     *
1514     * @param string        $folderid       id of the folder
1515     * @param string        $header         header of the message
1516     * @param long          $body           body of the message
1517     *
1518     * @access protected
1519     * @return boolean      status
1520     */
1521    protected function addSentMessage($folderid, $header, $body) {
1522        $header_body = str_replace("\n", "\r\n", str_replace("\r", "", $header . "\n\n" . $body));
1523
1524        return @imap_append($this->mbox, $this->server . $folderid, $header_body, "\\Seen");
1525    }
1526
1527    /**
1528     * Parses an mimedecode address array back to a simple "," separated string
1529     *
1530     * @param array         $ad             addresses array
1531     *
1532     * @access protected
1533     * @return string       mail address(es) string
1534     */
1535    protected function parseAddr($ad) {
1536        $addr_string = "";
1537        if (isset($ad) && is_array($ad)) {
1538            foreach($ad as $addr) {
1539                if ($addr_string) $addr_string .= ",";
1540                $addr_string .= $addr->mailbox . "@" . $addr->host;
1541            }
1542        }
1543        return $addr_string;
1544    }
1545
1546    /**
1547     * Recursive way to get mod and parent - repeat until only one part is left
1548     * or the folder is identified as an IMAP folder
1549     *
1550     * @param string        $fhir           folder hierarchy string
1551     * @param string        &$displayname   reference of the displayname
1552     * @param long          &$parent        reference of the parent folder
1553     *
1554     * @access protected
1555     * @return
1556     */
1557    protected function getModAndParentNames($fhir, &$displayname, &$parent) {
1558        // if mod is already set add the previous part to it as it might be a folder which has
1559        // delimiter in its name
1560        $displayname = (isset($displayname) && strlen($displayname) > 0) ? $displayname = array_pop($fhir).$this->serverdelimiter.$displayname : array_pop($fhir);
1561        $parent = implode($this->serverdelimiter, $fhir);
1562
1563        if (count($fhir) == 1 || $this->checkIfIMAPFolder($parent)) {
1564            return;
1565        }
1566        //recursion magic
1567        $this->getModAndParentNames($fhir, $displayname, $parent);
1568    }
1569
1570    /**
1571     * Checks if a specified name is a folder in the IMAP store
1572     *
1573     * @param string        $foldername     a foldername
1574     *
1575     * @access protected
1576     * @return boolean
1577     */
1578    protected function checkIfIMAPFolder($folderName) {
1579        $parent = imap_list($this->mbox, $this->server, $folderName);
1580        if ($parent === false) return false;
1581        return true;
1582    }
1583
1584    /**
1585     * Removes parenthesis (comments) from the date string because
1586     * strtotime returns false if received date has them
1587     *
1588     * @param string        $receiveddate   a date as a string
1589     *
1590     * @access protected
1591     * @return string
1592     */
1593    protected function cleanupDate($receiveddate) {
1594        $receiveddate = strtotime(preg_replace("/\(.*\)/", "", $receiveddate));
1595        if ($receiveddate == false || $receiveddate == -1) {
1596            debugLog("Received date is false. Message might be broken.");
1597            return null;
1598        }
1599
1600        return $receiveddate;
1601    }
1602
1603}
1604
1605?>
Note: See TracBrowser for help on using the repository browser.