Merge branch '2014.11-develop' into 2015.11
[tine20] / tine20 / Felamimail / Frontend / ActiveSync.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Felamimail
6  * @subpackage  Frontend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2008-2015 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>Lars Kneschke <l.kneschke@metaways.de>
10  */
11
12 /**
13  * ActiveSync frontend class
14  *
15  * @package     Felamimail
16  * @subpackage  Frontend
17  */
18 class Felamimail_Frontend_ActiveSync extends ActiveSync_Frontend_Abstract implements Syncroton_Data_IDataEmail, Syncroton_Data_IDataSearch
19 {
20     protected $_mapping = array(
21         'body'              => 'body',
22         'cc'                => 'cc',
23         'dateReceived'      => 'received',
24         'from'              => 'from_email',
25         #'Sender'            => 'sender',
26         'subject'           => 'subject',
27         'to'                => 'to'
28     );
29     
30     protected $_debugEmail = false;
31     
32     /**
33      * available filters
34      * 
35      * @var array
36      */
37     protected $_filterArray = array(
38         Syncroton_Command_Sync::FILTER_1_DAY_BACK,
39         Syncroton_Command_Sync::FILTER_3_DAYS_BACK,
40         Syncroton_Command_Sync::FILTER_1_WEEK_BACK,
41         Syncroton_Command_Sync::FILTER_2_WEEKS_BACK,
42         Syncroton_Command_Sync::FILTER_1_MONTH_BACK,
43     );
44     
45     /**
46      * felamimail message controller
47      *
48      * @var Felamimail_Controller_Message
49      */
50     protected $_messageController;
51     
52     /**
53      * felamimail account
54      * 
55      * @var Felamimail_Model_Account
56      */
57     protected $_account;
58     
59     /**
60      * app name (required by abstract class)
61      * 
62      * @var string
63      */
64     protected $_applicationName     = 'Felamimail';
65     
66     /**
67      * model name (required by abstract class)
68      * 
69      * @var string
70      */
71     protected $_modelName           = 'Message';
72     
73     /**
74      * type of the default folder
75      *
76      * @var int
77      */
78     protected $_defaultFolderType   = Syncroton_Command_FolderSync::FOLDERTYPE_INBOX;
79     
80     /**
81      * type of user created folders
82      *
83      * @var int
84      */
85     protected $_folderType          = Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED;
86     
87     /**
88      * name of property which defines the filterid for different content classes
89      * 
90      * @var string
91      */
92     protected $_filterProperty = 'emailfilterId';
93     
94     /**
95      * field to sort search results by
96      * 
97      * @var string
98      */
99     protected $_sortField = 'received';
100     
101     /**
102      * @var Felamimail_Controller_Message
103      */
104     protected $_contentController;
105     
106     /**
107      * prefix for faked folders
108      * 
109      * @var string
110      */
111     protected $_fakePrefix = 'fake-';
112     
113     /**
114      * (non-PHPdoc)
115      * @see Syncroton_Data_IDataEmail::forwardEmail()
116      */
117     public function forwardEmail($source, $inputStream, $saveInSent, $replaceMime)
118     {
119         $account = $this->_getAccount();
120         
121         if (!$account) {
122             throw new Syncroton_Exception('no email account configured');
123         }
124         
125         if (empty(Tinebase_Core::getUser()->accountEmailAddress)) {
126             throw new Syncroton_Exception('no email address set for current user');
127         }
128         
129         if (! is_resource($inputStream)) {
130             $stream = fopen("php://temp", 'r+');
131             fwrite($stream, $inputStream);
132             $inputStream = $stream;
133             rewind($inputStream);
134         }
135         
136          if ($this->_debugEmail == true) {
137              $debugStream = fopen("php://temp", 'r+');
138              stream_copy_to_stream($inputStream, $debugStream);
139              rewind($debugStream);
140              if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
141                  __METHOD__ . '::' . __LINE__ . " email to send:" . stream_get_contents($debugStream));
142         
143              // replace original stream with debug stream, as php://input can't be rewinded
144              $inputStream = $debugStream;
145              rewind($inputStream);
146          }
147         
148         $incomingMessage = new Zend_Mail_Message(
149             array(
150                 'file' => $inputStream
151             )
152         );
153         
154         $messageId = is_array($source) ? $source['itemId'] : $source;
155         $fmailMessage = Felamimail_Controller_Message::getInstance()->get($messageId);
156         $fmailMessage->flags = Zend_Mail_Storage::FLAG_PASSED;
157         
158         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
159             __METHOD__ . '::' . __LINE__ . " source: " . $messageId . "saveInSent: " . $saveInSent);
160         
161         if ($replaceMime === FALSE) {
162             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
163                  __METHOD__ . '::' . __LINE__ . " Adding RFC822 attachment and appending body to forward message.");
164             
165             $rfc822 = Felamimail_Controller_Message::getInstance()->getMessagePart($fmailMessage);
166             $rfc822->type = Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822;
167             $rfc822->filename = 'forwarded_email.eml';
168             $rfc822->encoding = Zend_Mime::ENCODING_7BIT;
169             $replyBody = Felamimail_Controller_Message::getInstance()->getMessageBody($fmailMessage, NULL, 'text/plain');
170         } else {
171             $rfc822 = NULL;
172             $replyBody = NULL;
173         }
174         
175         $mail = Tinebase_Mail::createFromZMM($incomingMessage, $replyBody);
176         if ($rfc822) {
177             $mail->addAttachment($rfc822);
178         }
179         
180         Felamimail_Controller_Message_Send::getInstance()->sendZendMail($account, $mail, $saveInSent, $fmailMessage);
181     }
182     
183     /**
184      * get all entries changed between to dates
185      *
186      * @param unknown_type $_field
187      * @param unknown_type $_startTimeStamp
188      * @param unknown_type $_endTimeStamp
189      * @return array
190      */
191     public function getChangedEntries($folderId, DateTime $_startTimeStamp, DateTime $_endTimeStamp = NULL, $filterType = NULL)
192     {
193         if (strpos($folderId, $this->_fakePrefix) === 0) {
194             return array();
195         }
196         
197         $filter = $this->_getContentFilter(0);
198         
199         $this->_addContainerFilter($filter, $folderId);
200
201         $startTimeStamp = ($_startTimeStamp instanceof DateTime) ? $_startTimeStamp->format(Tinebase_Record_Abstract::ISO8601LONG) : $_startTimeStamp;
202         $endTimeStamp = ($_endTimeStamp instanceof DateTime) ? $_endTimeStamp->format(Tinebase_Record_Abstract::ISO8601LONG) : $_endTimeStamp;
203         
204         // @todo filter also for create_timestamp??
205         $filter->addFilter(new Tinebase_Model_Filter_DateTime(
206             'timestamp',
207             'after',
208             $startTimeStamp
209         ));
210         
211         if($endTimeStamp !== NULL) {
212             $filter->addFilter(new Tinebase_Model_Filter_DateTime(
213                 'timestamp',
214                 'before',
215                 $endTimeStamp
216             ));
217         }
218         
219         $result = $this->_contentController->search($filter, NULL, false, true, 'sync');
220         
221         return $result;
222     }
223     
224     /**
225      * retrieve folders which were modified since last sync
226      * 
227      * @param  DateTime $startTimeStamp
228      * @param  DateTime $endTimeStamp
229      * @return array
230      * 
231      * @todo implement
232      * 
233      * @see 0007786: changed email folder names do not sync to device
234      */
235     public function getChangedFolders(DateTime $startTimeStamp, DateTime $endTimeStamp)
236     {
237         $syncrotonFolders = array();
238         
239         // @todo calculate changed folders
240         
241         return $syncrotonFolders;
242     }
243     
244     /**
245      * (non-PHPdoc)
246      * @see ActiveSync_Frontend_Abstract::getFileReference()
247      */
248     public function getFileReference($fileReference)
249     {
250         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
251             __METHOD__ . '::' . __LINE__ . " fileReference " . $fileReference);
252         
253         list($messageId, $partId) = explode(ActiveSync_Frontend_Abstract::LONGID_DELIMITER, $fileReference, 2);
254         
255         $part = $this->_contentController->getMessagePart($messageId, $partId);
256         
257         $syncrotonFileReference = new Syncroton_Model_FileReference(array(
258             'contentType' => $part->type,
259             'data'        => $part->getDecodedStream()
260         ));
261         
262         return $syncrotonFileReference;
263     }
264     
265     /**
266      * (non-PHPdoc)
267      * @see Syncroton_Data_IDataEmail::replyEmail()
268      */
269     public function replyEmail($source, $inputStream, $saveInSent, $replaceMime)
270     {
271         $account = $this->_getAccount();
272         
273         if (!$account) {
274             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(
275                 __METHOD__ . '::' . __LINE__ . " no email account configured");
276             
277             throw new Syncroton_Exception('no email account configured');
278         }
279         
280         if (empty(Tinebase_Core::getUser()->accountEmailAddress)) {
281             throw new Syncroton_Exception('no email address set for current user');
282         }
283         
284         if (! is_resource($inputStream)) {
285             $stream = fopen("php://temp", 'r+');
286             fwrite($stream, $inputStream);
287             $inputStream = $stream;
288             rewind($inputStream);
289         }
290         
291         if ($this->_debugEmail == true) {
292              $debugStream = fopen("php://temp", 'r+');
293              stream_copy_to_stream($inputStream, $debugStream);
294              rewind($debugStream);
295              if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
296                  __METHOD__ . '::' . __LINE__ . " email to send:" . stream_get_contents($debugStream));
297         
298              //replace original stream wirh debug stream, as php://input can't be rewinded
299              $inputStream = $debugStream;
300              rewind($inputStream);
301         }
302         
303         $incomingMessage = new Zend_Mail_Message(
304             array(
305                 'file' => $inputStream
306             )
307         );
308         
309         $messageId = is_array($source) ? $source['itemId'] : $source;
310         $fmailMessage = Felamimail_Controller_Message::getInstance()->get($messageId);
311         $fmailMessage->flags = Zend_Mail_Storage::FLAG_ANSWERED;
312         
313         if ($replaceMime === false) {
314             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
315                 __METHOD__ . '::' . __LINE__ . " attach source: " . $messageId . " saveInSent: " . $saveInSent);
316             
317             $replyBody = Felamimail_Controller_Message::getInstance()->getMessageBody($fmailMessage, null, 'text/plain');
318         } else {
319             $replyBody = null;
320         }
321         
322         $mail = Tinebase_Mail::createFromZMM($incomingMessage, $replyBody);
323         
324         Felamimail_Controller_Message_Send::getInstance()->sendZendMail($account, $mail, (bool)$saveInSent, $fmailMessage);
325     }
326     
327     /**
328      * send email
329      * 
330      * @param resource $inputStream
331      * @param boolean $saveInSent
332      * @throws Syncroton_Exception
333      */
334     public function sendEmail($inputStream, $saveInSent)
335     {
336         $account = $this->_getAccount();
337         
338         if (!$account) {
339             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(
340                 __METHOD__ . '::' . __LINE__ . " no email account configured");
341             
342             throw new Syncroton_Exception('no email account configured');
343         }
344         
345         if (empty(Tinebase_Core::getUser()->accountEmailAddress)) {
346             throw new Syncroton_Exception('no email address set for current user');
347         }
348         
349         if (! is_resource($inputStream)) {
350             $stream = fopen("php://temp", 'r+');
351             fwrite($stream, $inputStream);
352             $inputStream = $stream;
353             rewind($inputStream);
354         }
355         
356         if ($this->_debugEmail == true) {
357              $debugStream = fopen("php://temp", 'r+');
358              stream_copy_to_stream($inputStream, $debugStream);
359              rewind($debugStream);
360              if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
361                  __METHOD__ . '::' . __LINE__ . " email to send:" . stream_get_contents($debugStream));
362         
363              //replace original stream wirh debug stream, as php://input can't be rewinded
364              $inputStream = $debugStream;
365              rewind($inputStream);
366         }
367         
368         $incomingMessage = new Zend_Mail_Message(
369             array(
370                 'file' => $inputStream
371             )
372         );
373         
374         $subject = $incomingMessage->headerExists('subject') ? $incomingMessage->getHeader('subject') : null;
375         
376         if (Tinebase_Mail::isiMIPMail($incomingMessage)) {
377             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
378                 . ' Do not send iMIP message with subject "' . $subject . '". The server should handle those.');
379         } else {
380             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
381                 __METHOD__ . '::' . __LINE__ . " Send Message with subject " . $subject . " (saveInSent: " . $saveInSent . ")");
382             
383             $mail = Tinebase_Mail::createFromZMM($incomingMessage);
384         
385             Felamimail_Controller_Message_Send::getInstance()->sendZendMail($account, $mail, (bool)$saveInSent);
386         }
387     }
388     
389     /**
390      * (non-PHPdoc)
391      * @see ActiveSync_Frontend_Abstract::toSyncrotonModel()
392      */
393     public function toSyncrotonModel($entry, array $options = array())
394     {
395         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
396             __METHOD__ . '::' . __LINE__ . " email data " . print_r($entry->toArray(), true));
397         
398         if (! $this->_isMemoryLeft($entry->size)) {
399             throw new Syncroton_Exception_MemoryExhausted('not enough memory left for: ' . $entry->getId() . ' Needed: ' . $entry->size);
400         }
401         
402         $syncrotonEmail = new Syncroton_Model_Email();
403         
404         foreach ($this->_mapping as $syncrotonProperty => $tine20Property) {
405             if (empty($entry->$tine20Property) && $entry->$tine20Property != '0' || count($entry->$tine20Property) === 0) {
406                 continue;
407             }
408         
409             switch($tine20Property) {
410                 case 'from_email':
411                     $syncrotonEmail->$syncrotonProperty = $this->_createEmailAddress($entry->from_name, $entry->from_email);
412                     
413                     break;
414                     
415                 case 'to':
416                 case 'cc':
417                     $syncrotonEmail->$syncrotonProperty = implode(', ', $entry->$tine20Property);
418                     
419                     break;
420                     
421                 default:
422                     $syncrotonEmail->$syncrotonProperty = $entry->$tine20Property;
423                     break;
424             }
425         }
426         
427         $syncrotonEmail->body = $this->_getSyncrotonBody($entry, $options);
428         if ($syncrotonEmail->body->type < 4) {
429             $syncrotonEmail->nativeBodyType = $syncrotonEmail->body->type;
430         }
431         
432         if ($syncrotonEmail->body->type == Syncroton_Command_Sync::BODY_TYPE_MIME) {
433             $syncrotonEmail->messageClass = 'IPM.Note.SMIME';
434         } else {
435             $syncrotonEmail->messageClass = 'IPM.Note';
436         }
437         
438         $syncrotonEmail->contentClass = 'urn:content-classes:message';
439         
440         // read flag
441         $syncrotonEmail->read = in_array(Zend_Mail_Storage::FLAG_SEEN, $entry->flags) ? 1 : 0;
442         
443         if (in_array(Zend_Mail_Storage::FLAG_ANSWERED, $entry->flags)) {
444             $syncrotonEmail->lastVerbExecuted = Syncroton_Model_Email::LASTVERB_REPLYTOSENDER;
445             $syncrotonEmail->lastVerbExecutionTime = new DateTime('now', new DateTimeZone('utc'));
446         #} elseif (in_array('\Forwarded', $entry->flags)) {
447         #    $syncrotonEmail->lastVerbExecuted = Syncroton_Model_Email::LASTVERB_FORWARD;
448         #    $syncrotonEmail->lastVerbExecutionTime = new DateTime('now', new DateTimeZone('utc'));
449         }
450         
451         $syncrotonEmail->flag = in_array('\Flagged', $entry->flags) ? 
452             new Syncroton_Model_EmailFlag(array(
453                 'status'       => Syncroton_Model_EmailFlag::STATUS_ACTIVE,
454                 'flagType'     => 'FollowUp',
455                 'reminderSet'  => 0,
456                 'startDate'    => Tinebase_DateTime::now(),
457                 'utcStartDate' => Tinebase_DateTime::now(),
458                 'dueDate'    => Tinebase_DateTime::now()->addWeek(1),
459                 'utcDueDate' => Tinebase_DateTime::now()->addWeek(1),
460             )) : 
461             new Syncroton_Model_EmailFlag(array(
462                 'status' => Syncroton_Model_EmailFlag::STATUS_CLEARED
463             ));
464         
465         // attachments?
466         if ($entry->has_attachment == true) {
467             $syncrotonAttachments = array();
468             
469             $attachments = $this->_contentController->getAttachments($entry);
470
471             if (count($attachments) > 0) {
472                 foreach ($attachments as $attachment) {
473                     $syncrotonAttachment = new Syncroton_Model_EmailAttachment(array(
474                         'displayName'       => trim($attachment['filename']),
475                         'fileReference'     => $entry->getId() . ActiveSync_Frontend_Abstract::LONGID_DELIMITER . $attachment['partId'],
476                         'method'            => 1,
477                         'estimatedDataSize' => $attachment['size']
478                     ));
479                     
480                     $syncrotonAttachments[] = $syncrotonAttachment;
481                 }
482             }
483             
484             $syncrotonEmail->attachments = $syncrotonAttachments;
485         }
486         
487         
488         #$syncrotonEmail->categories = array('Test');
489         $syncrotonEmail->conversationId    = $entry->getId();
490         $syncrotonEmail->conversationIndex = "\x00\x01\x02\x03\x04";
491         
492         return $syncrotonEmail;
493     }
494     
495     /**
496      * (non-PHPdoc)
497      * @see Syncroton_Data_IDataSearch::search()
498      */
499     public function search(Syncroton_Model_StoreRequest $store)
500     {
501         $storeResponse = new Syncroton_Model_StoreResponse();
502         
503         if (!isset($store->query['and']) || !isset($store->query['and']['freeText'])) {
504             $storeResponse->total = 0;
505             return $storeResponse;
506         }
507         
508         $filter = new $this->_contentFilterClass(array(array(
509             'field'     => 'query',
510             'operator'  => 'contains',
511             'value'     => $store->query['and']['freeText']
512         )));
513         
514         if (isset($store->query['and']['collections'])) {
515             // @todo search for multiple folders
516             $folderId = $store->query['and']['collections'][0];
517         } else {
518             $folderId = Felamimail_Controller_Folder::getInstance()->getByBackendAndGlobalName(
519                 Tinebase_Core::getPreference('Felamimail')->{Felamimail_Preference::DEFAULTACCOUNT},
520                 'INBOX'
521             )->getId();
522         }
523         $this->_addContainerFilter($filter, $folderId);
524         
525         if (isset($store->options['range'])) {
526             $pagination = new Tinebase_Model_Pagination(array(
527                 'start' => $store->options['range'][0],
528                 'limit' => $store->options['range'][1] - $store->options['range'][0],
529                 'sort' => $this->_sortField,
530                 'dir' => 'DESC',
531             ));
532         } else {
533             $pagination = new Tinebase_Model_Pagination(array(
534                 'sort' => $this->_sortField,
535                 'dir' => 'DESC',
536             ));
537         }
538         
539         $serverIds = $this->_contentController->search($filter, $pagination, false, true, 'sync');
540         $totalCount = $this->_contentController->searchCount($filter, 'sync');
541         
542         foreach ($serverIds as $serverId) {
543             $email = $this->getEntry(
544                 new Syncroton_Model_SyncCollection(array(
545                     'collectionId' => $folderId,
546                     'options'      => $store->options
547                 )), 
548                 $serverId
549             );
550     
551             $storeResponse->result[] = new Syncroton_Model_StoreResponseResult(array(
552                 'class'        => 'Email',
553                 'longId'       => $folderId . ActiveSync_Frontend_Abstract::LONGID_DELIMITER . $serverId,
554                 'collectionId' => $folderId,
555                 'properties'   => $email
556             ));
557         }
558         
559         $storeResponse->total = $totalCount;
560         if (count($storeResponse->result) > 0) {
561             $storeResponse->range = array($store->options['range'][0], $store->options['range'][1]);
562         }
563         
564         return $storeResponse;
565     }
566     
567     /**
568      * 
569      * @param Felamimail_Model_Message $entry
570      * @param array $options
571      * @return void|Syncroton_Model_EmailBody
572      */
573     protected function _getSyncrotonBody(Felamimail_Model_Message $entry, $options)
574     {
575         //var_dump($options);
576         
577         // get truncation
578         $truncateAt  = null;
579         $previewSize = null;
580         
581         if ($options['mimeSupport'] == Syncroton_Command_Sync::MIMESUPPORT_SEND_MIME) {
582             $airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_MIME;
583             
584             if (isset($options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_MIME]['truncationSize'])) {
585                 $truncateAt = $options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_MIME]['truncationSize'];
586             } elseif (isset($options['mimeTruncation']) && $options['mimeTruncation'] < Syncroton_Command_Sync::TRUNCATE_NOTHING) {
587                 switch($options['mimeTruncation']) {
588                     case Syncroton_Command_Sync::TRUNCATE_ALL:
589                         $truncateAt = 0;
590                         break;
591                     case Syncroton_Command_Sync::TRUNCATE_4096:
592                         $truncateAt = 4096;
593                         break;
594                     case Syncroton_Command_Sync::TRUNCATE_5120:
595                         $truncateAt = 5120;
596                         break;
597                     case Syncroton_Command_Sync::TRUNCATE_7168:
598                         $truncateAt = 7168;
599                         break;
600                     case Syncroton_Command_Sync::TRUNCATE_10240:
601                         $truncateAt = 10240;
602                         break;
603                     case Syncroton_Command_Sync::TRUNCATE_20480:
604                         $truncateAt = 20480;
605                         break;
606                     case Syncroton_Command_Sync::TRUNCATE_51200:
607                         $truncateAt = 51200;
608                         break;
609                     case Syncroton_Command_Sync::TRUNCATE_102400:
610                         $truncateAt = 102400;
611                         break;
612                 }
613             }
614             
615         } elseif (isset($options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_HTML])) {
616             $airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_HTML;
617             
618             if (isset($options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_HTML]['truncationSize'])) {
619                 $truncateAt = $options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_HTML]['truncationSize'];
620             }
621             if (isset($options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_HTML]['preview'])) {
622                 $previewSize = $options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_HTML]['preview'];
623             }
624             
625         } else {
626             $airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT;
627             
628             if (isset($options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT]['truncationSize'])) {
629                 $truncateAt = $options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT]['truncationSize'];
630             }
631             if (isset($options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT]['preview'])) {
632                 $previewSize = $options['bodyPreferences'][Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT]['preview'];
633             }
634         }
635         
636         if ($airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_MIME) {
637             // getMessagePart will return Zend_Mime_Part
638             $messageBody = $this->_contentController->getMessagePart($entry);
639             $messageBody = stream_get_contents($messageBody->getRawStream());
640             
641         } else {
642             $messageBody = $this->_contentController->getMessageBody($entry, null, $airSyncBaseType == 2 ? Zend_Mime::TYPE_HTML : Zend_Mime::TYPE_TEXT, NULL, true);
643         }
644         
645         if($previewSize !== null) {
646             $preview = substr($this->_contentController->getMessageBody($entry, null, Zend_Mime::TYPE_TEXT, NULL, true), 0, $previewSize);
647             
648             // strip out any non utf-8 characters
649             $preview = @iconv('utf-8', 'utf-8//IGNORE', $preview);
650         }
651         
652         if($truncateAt !== null && strlen($messageBody) > $truncateAt) {
653             $messageBody  = substr($messageBody, 0, $truncateAt);
654             $isTruncacted = 1;
655         } else {
656             $isTruncacted = 0;
657         }
658         
659         // strip out any non utf-8 characters
660         $messageBody  = @iconv('utf-8', 'utf-8//IGNORE', $messageBody);
661         
662         $synrotonBody = new Syncroton_Model_EmailBody(array(
663             'type'              => $airSyncBaseType,
664             'estimatedDataSize' => $entry->size,
665         ));
666         
667         if (strlen($messageBody) > 0) {
668             $synrotonBody->data = $messageBody;
669         }
670         
671         if (isset($preview) && strlen($preview) > 0) {
672             $synrotonBody->preview = $preview;
673         }
674         
675         if ($isTruncacted === 1) {
676             $synrotonBody->truncated = 1;
677         } else {
678             $synrotonBody->truncated = 0;
679         }
680         
681         return $synrotonBody;
682     }
683     
684     /**
685      * (non-PHPdoc)
686      * @see ActiveSync_Frontend_Abstract::_inspectGetCountOfChanges()
687      */
688     protected function _inspectGetCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
689     {
690         if (strpos($folder->serverId, $this->_fakePrefix) === false) {
691             Felamimail_Controller_Cache_Message::getInstance()->updateCache($folder->serverId, 10);
692         }
693     }
694     
695     /**
696      * delete entry
697      *
698      * @param  string  $_folderId
699      * @param  string  $_serverId
700      * @param  array   $_collectionData
701      */
702     public function deleteEntry($_folderId, $_serverId, $_collectionData)
703     {
704         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " delete ColectionId: $_folderId Id: $_serverId");
705         
706         $folder  = Felamimail_Controller_Folder::getInstance()->get($_folderId);
707         $account = Felamimail_Controller_Account::getInstance()->get($folder->account_id);
708         
709         if ($_collectionData->deletesAsMoves === true && !empty($account->trash_folder)) {
710             // move message to trash folder
711             $trashFolder = Felamimail_Controller_Folder::getInstance()->getByBackendAndGlobalName($account, $account->trash_folder);
712             Felamimail_Controller_Message_Move::getInstance()->moveMessages($_serverId, $trashFolder);
713             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " moved entry $_serverId to trash folder");
714         } else {
715             // set delete flag
716             Felamimail_Controller_Message_Flags::getInstance()->addFlags($_serverId, Zend_Mail_Storage::FLAG_DELETED);
717             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " deleted entry " . $_serverId);
718         }
719     }
720     
721     /**
722      * (non-PHPdoc)
723      * @see ActiveSync_Frontend_Abstract::updateEntry()
724      */
725     public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry)
726     {
727         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
728             __METHOD__ . '::' . __LINE__ . " CollectionId: $folderId Id: $serverId");
729         
730         try {
731             $message = $this->_contentController->get($serverId);
732         } catch (Tinebase_Exception_NotFound $tenf) {
733             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
734                 __METHOD__ . '::' . __LINE__ . ' ' . $tenf);
735             throw new Syncroton_Exception_NotFound($tenf->getMessage());
736         }
737         
738         if(isset($entry->read)) {
739             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
740                 __METHOD__ . '::' . __LINE__ . " CollectionId: $folderId Id: $serverId set read flag: $entry->read");
741             
742             if($entry->read == 1) {
743                 Felamimail_Controller_Message_Flags::getInstance()->addFlags($serverId, Zend_Mail_Storage::FLAG_SEEN);
744             } else {
745                 Felamimail_Controller_Message_Flags::getInstance()->clearFlags($serverId, Zend_Mail_Storage::FLAG_SEEN);
746             }
747         }
748         
749         if(isset($entry->flag)) {
750             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
751                 __METHOD__ . '::' . __LINE__ . " CollectionId: $folderId Id: $serverId set flagged flag: {$entry->flag->status}");
752             
753             if($entry->flag->status == Syncroton_Model_EmailFlag::STATUS_ACTIVE) {
754                 Felamimail_Controller_Message_Flags::getInstance()->addFlags($serverId, Zend_Mail_Storage::FLAG_FLAGGED);
755             } else {
756                 Felamimail_Controller_Message_Flags::getInstance()->clearFlags($serverId, Zend_Mail_Storage::FLAG_FLAGGED);
757             }
758         }
759         
760         $message->timestamp = $this->_syncTimeStamp;
761         $this->_contentController->update($message);
762         
763         return;
764     }
765     
766     /**
767      * (non-PHPdoc)
768      * @see Syncroton_Data_IData::updateFolder()
769      */
770     public function updateFolder(Syncroton_Model_IFolder $folder)
771     {
772         if (strpos($folderId, $this->_fakePrefix) === 0) {
773             return $folder;
774         }
775         
776         $fmailFolder = Felamimail_Controller_Folder::getInstance()->get($folder->serverId);
777         Felamimail_Controller_Folder::getInstance()->rename($fmailFolder->account_id, $folder->displayName, $fmailFolder->globalname);
778         return $folder;
779     }
780     
781     /**
782      * (non-PHPdoc)
783      * @see ActiveSync_Frontend_Abstract::toTineModel()
784      */
785     public function toTineModel(Syncroton_Model_IEntry $data, $entry = null)
786     {
787         // does nothing => you can't add emails via ActiveSync
788     }
789     
790     /**
791      * create rfc email address 
792      * 
793      * @param  string  $_realName
794      * @param  string  $_address
795      * @return string
796      */
797     protected function _createEmailAddress($_realName, $_address)
798     {
799         return !empty($_realName) ? sprintf('"%s" <%s>', str_replace('"', '\\"', $_realName), $_address) : $_address;
800     }
801     
802     /**
803      * return list of supported folders for this backend
804      *
805      * @return array
806      */
807     public function getAllFolders()
808     {
809         if (!Tinebase_Core::getUser()->hasRight('Felamimail', Tinebase_Acl_Rights::RUN)) {
810             // no folders
811             return array();
812         }
813         
814         $this->_updateFolderCache();
815         $result = $this->_getFolders();
816         
817         $this->_addFakeFolders($result);
818         
819         return $result;
820     }
821     
822     /**
823      * get felamimail account
824      * 
825      * @return Felamimail_Model_Account|NULL
826      */
827     protected function _getAccount()
828     {
829         if ($this->_account === NULL) {
830             $defaultAccountId = Tinebase_Core::getPreference('Felamimail')->{Felamimail_Preference::DEFAULTACCOUNT};
831             
832             if (empty($defaultAccountId)) {
833                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
834                     __METHOD__ . '::' . __LINE__ . " no default account set. Can't sync any folders.");
835                 return NULL;
836             }
837             
838             try {
839                 $this->_account = Felamimail_Controller_Account::getInstance()->get($defaultAccountId);
840             } catch (Tinebase_Exception_NotFound $ten) {
841                 return NULL;
842             }
843         }
844         
845         return $this->_account;
846     }
847     
848     /**
849      * update the folder cache
850      * 
851      * @throws Syncroton_Exception_Status_FolderSync
852      */
853     protected function _updateFolderCache()
854     {
855         $account = $this->_getAccount();
856         if (! $account) {
857             return;
858         }
859         
860         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
861             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " accountData " . print_r($account->toArray(), true));
862         
863         try {
864             Felamimail_Controller_Cache_Folder::getInstance()->update($account);
865             
866         } catch (Felamimail_Exception_IMAPServiceUnavailable $feisu) {
867             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(
868                 __METHOD__ . '::' . __LINE__ . " Could not update folder cache: " . $feisu);
869             throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR);
870             
871         } catch (Felamimail_Exception_IMAPInvalidCredentials $feiic) {
872             Tinebase_Exception::log($feiic, null, array(
873                 'accountname' => $account->name
874             ));
875         }
876     }
877     
878     /**
879      * get Syncroton_Model_Folder folders recursively by parentFolder
880      * 
881      * @param Felamimail_Model_Folder $parentFolder
882      * @param array $result
883      * @return array of Syncroton_Model_Folder
884      */
885     protected function _getFolders($parentFolder = NULL, &$result = array())
886     {
887         $globalname = ($parentFolder) ? $parentFolder->globalname : '';
888         $account = $this->_getAccount();
889         if (! $account) {
890             return array();
891         }
892         
893         $filter = new Felamimail_Model_FolderFilter(array(
894             array('field' => 'globalname', 'operator' => 'startswith',  'value' => $globalname),
895             array('field' => 'account_id', 'operator' => 'equals',      'value' => $account->getId()),
896         ));
897         
898         try {
899             $folders = Felamimail_Controller_Folder::getInstance()->search($filter);
900         } catch (Felamimail_Exception_IMAPInvalidCredentials $feiic) {
901             Tinebase_Exception::log($feiic, null, array(
902                 'accountname' => $account->name
903             ));
904             return array();
905         }
906         
907         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
908             . " Found " . count($folders) . ' subfolders of folder "' . $globalname . '"');
909         
910         foreach ($folders as $folder) {
911             $result[$folder->getId()] = new Syncroton_Model_Folder(array(
912                 'serverId'    => $folder->getId(),
913                 'parentId'    => ($parentFolder) ? $parentFolder->getId() : 0,
914                 'displayName' => $folder->localname,
915                 'type'        => $this->_getFolderType($folder)
916             ));
917             
918             if ($folder->has_children) {
919                 $this->_getFolders($folder, $result);
920             }
921         }
922         
923         return $result;
924     }
925     
926     /**
927      * (non-PHPdoc)
928      * @see ActiveSync_Frontend_Abstract::moveItem()
929      */
930     public function moveItem($srcFolderId, $serverId, $dstFolderId)
931     {
932         $filter = new Felamimail_Model_MessageFilter(array(
933             array(
934                 'field'     => 'id',
935                 'operator'  => 'equals',
936                 'value'     => $serverId
937             )
938         ));
939         
940         Felamimail_Controller_Message_Move::getInstance()->moveMessages($filter, $dstFolderId);
941         
942         return $serverId;
943     }
944     
945     /**
946      * used by the mail backend only. Used to update the folder cache
947      * 
948      * @param  string  $_folderId
949      */
950     public function updateCache($_folderId)
951     {
952         try {
953             Felamimail_Controller_Cache_Message::getInstance()->updateCache($_folderId, 5);
954         } catch (Exception $e) {
955             if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
956                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " catched exception " . get_class($e));
957             if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
958                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " " . $e->getMessage());
959             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
960                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString());
961         }
962     }
963     
964     /**
965      * set activesync foldertype
966      * 
967      * @param Felamimail_Model_Folder $folder
968      */
969     protected function _getFolderType(Felamimail_Model_Folder $folder)
970     {
971         $personalNameSpaceSuffix = null;
972
973         $account = $this->_getAccount();
974         
975         // first lookup folder type by account settings ...
976         if ($account && !empty($account->ns_personal)) {
977             $personalNameSpaceSuffix = $account->ns_personal . $account->delimiter;
978         }
979         
980         switch (strtoupper($folder->localname)) {
981             case 'INBOX':
982                 if (($personalNameSpaceSuffix . 'INBOX' === $folder->globalname) ||
983                     (substr($personalNameSpaceSuffix, 0, 5) === 'INBOX' && $folder->globalname === 'INBOX') // Cyrus Prvate Namespace == 'INBOX.'
984                 ) {
985                     return Syncroton_Command_FolderSync::FOLDERTYPE_INBOX;
986                 }
987                 
988                 break;
989                 
990             case 'TRASH':
991                 // either use configured trash folder or detect by name
992                 if (($account && $account->trash_folder === $folder->globalname) ||
993                     ($personalNameSpaceSuffix . $folder->localname === $folder->globalname)
994                 ) {
995                     return Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS;
996                 }
997                 
998                 break;
999                 
1000             case 'SENT':
1001                 // either use configured sent folder or detect by name
1002                 if (($account && $account->sent_folder === $folder->globalname) ||
1003                     ($personalNameSpaceSuffix . $folder->localname === $folder->globalname)
1004                 ) {
1005                     return Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL;
1006                 }
1007                 
1008                 break;
1009                 
1010             case 'DRAFTS':
1011                 // either use configured drafts folder or detect by name
1012                 if (($account && $account->drafts_folder === $folder->globalname) ||
1013                     ($personalNameSpaceSuffix . $folder->localname === $folder->globalname)
1014                 ) {
1015                     return Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS;
1016                 }
1017                 
1018                 break;
1019                 
1020             case 'OUTBOX':
1021                 // detect by name
1022                 if ($personalNameSpaceSuffix . $folder->localname === $folder->globalname) {
1023                     return Syncroton_Command_FolderSync::FOLDERTYPE_OUTBOX;
1024                 }
1025                 
1026                 break;
1027         }
1028         
1029         return Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED;
1030     }
1031     
1032     /**
1033      * get folder identified by $_folderId
1034      *
1035      * @param string $_folderId
1036      * @return string
1037      */
1038     private function getFolder($_folderId)
1039     {
1040         $folders = $this->getSupportedFolders();
1041         
1042         if(!(isset($folders[$_folderId]) || array_key_exists($_folderId, $folders))) {
1043             throw new ActiveSync_Exception_FolderNotFound('folder not found. ' . $_folderId);
1044         }
1045         
1046         return $folders[$_folderId];
1047     }
1048     
1049     /**
1050      * (non-PHPdoc)
1051      * @see ActiveSync_Frontend_Abstract::_getContentFilter()
1052      */
1053     protected function _getContentFilter($_filterType)
1054     {
1055         $filter = parent::_getContentFilter($_filterType);
1056         
1057         if(in_array($_filterType, $this->_filterArray)) {
1058             $today = Tinebase_DateTime::now()->setTime(0,0,0);
1059                 
1060             switch($_filterType) {
1061                 case Syncroton_Command_Sync::FILTER_1_DAY_BACK:
1062                     $received = $today->subDay(1);
1063                     break;
1064                 case Syncroton_Command_Sync::FILTER_3_DAYS_BACK:
1065                     $received = $today->subDay(3);
1066                     break;
1067                 case Syncroton_Command_Sync::FILTER_1_WEEK_BACK:
1068                     $received = $today->subWeek(1);
1069                     break;
1070                 case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK:
1071                     $received = $today->subWeek(2);
1072                     break;
1073                 case Syncroton_Command_Sync::FILTER_1_MONTH_BACK:
1074                     $received = $today->subMonth(1);
1075                     break;
1076             }
1077             
1078             // add period filter
1079             $filter->addFilter(new Tinebase_Model_Filter_DateTime('received', 'after', $received->get(Tinebase_Record_Abstract::ISO8601LONG)));
1080         }
1081         
1082         return $filter;
1083     }
1084     
1085     /**
1086      * (non-PHPdoc)
1087      * @see ActiveSync_Frontend_Abstract::_addContainerFilter()
1088      */
1089     protected function _addContainerFilter(Tinebase_Model_Filter_FilterGroup $_filter, $_containerId)
1090     {
1091         // custom filter gets added when created
1092         $_filter->createFilter(
1093             'account_id', 
1094             'equals', 
1095             Tinebase_Core::getPreference('Felamimail')->{Felamimail_Preference::DEFAULTACCOUNT}
1096         );
1097         
1098         $_filter->addFilter($_filter->createFilter(
1099             'folder_id', 
1100             'equals', 
1101             $_containerId
1102         ));
1103     }
1104     
1105     /**
1106      * It creates faked folders that is required by Android 5 (Lollipop),
1107      * when it doesn't exists in IMAP
1108      *
1109      * @param array $folders
1110      */
1111     protected function _addFakeFolders(&$folders)
1112     {
1113         $requiredFolderTypes = array(
1114             Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS       => 'Drafts',
1115             Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS => 'Trash',
1116             Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL     => 'Sent',
1117             Syncroton_Command_FolderSync::FOLDERTYPE_OUTBOX       => 'Outbox'
1118         );
1119         
1120         $foundFolderTypes = array();
1121         
1122         foreach ($folders as $folder) {
1123             if ($folder->type >= Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS && $folder->type <= Syncroton_Command_FolderSync::FOLDERTYPE_OUTBOX) {
1124                 $foundFolderTypes[] = $folder->type;
1125             }
1126         }
1127         
1128         $missingFolderTypes = array_diff(array_keys($requiredFolderTypes), $foundFolderTypes);
1129         
1130         foreach ($missingFolderTypes as $folderType) {
1131            $fakedServerId = $this->_fakePrefix . $folderType;
1132            
1133            $folders[$fakedServerId] = new Syncroton_Model_Folder(array(
1134                 'serverId'      => $fakedServerId,
1135                 'parentId'      => 0,
1136                 'displayName'   => $requiredFolderTypes[$folderType],
1137                 'type'          => $folderType
1138             ));
1139         }
1140     }
1141     
1142     /**
1143      * 
1144      * @return int     Syncroton_Command_Sync::FILTER...
1145      */
1146     public function getMaxFilterType()
1147     {
1148         return ActiveSync_Config::getInstance()->get(ActiveSync_Config::MAX_FILTER_TYPE_EMAIL);
1149     }
1150 }