Merge branch '2016.11-develop' into 2017.02
[tine20] / tine20 / Felamimail / Controller / Message / Send.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Felamimail
6  * @subpackage  Controller
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Philipp Schüle <p.schuele@metaways.de>
9  * @copyright   Copyright (c) 2011-2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * send message controller for Felamimail
14  *
15  * @package     Felamimail
16  * @subpackage  Controller
17  */
18 class Felamimail_Controller_Message_Send extends Felamimail_Controller_Message
19 {
20     /**
21      * holds the instance of the singleton
22      *
23      * @var Felamimail_Controller_Message_Send
24      */
25     private static $_instance = NULL;
26     
27     /**
28      * the constructor
29      *
30      * don't use the constructor. use the singleton
31      */
32     private function __construct() 
33     {
34         $this->_backend = new Felamimail_Backend_Cache_Sql_Message();
35     }
36     
37     /**
38      * don't clone. Use the singleton.
39      *
40      */
41     private function __clone() 
42     {
43     }
44     
45     /**
46      * the singleton pattern
47      *
48      * @return Felamimail_Controller_Message_Send
49      */
50     public static function getInstance() 
51     {
52         if (self::$_instance === NULL) {
53             self::$_instance = new Felamimail_Controller_Message_Send();
54         }
55         
56         return self::$_instance;
57     }
58     
59     /**
60      * send one message through smtp
61      * 
62      * @param Felamimail_Model_Message $_message
63      * @return Felamimail_Model_Message
64      * @throws Tinebase_Exception_SystemGeneric
65      */
66     public function sendMessage(Felamimail_Model_Message $_message)
67     {
68         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
69             ' Sending message with subject ' . $_message->subject . ' to ' . print_r($_message->to, TRUE));
70         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_message->toArray(), TRUE));
71         
72         // increase execution time (sending message with attachments can take a long time)
73         $oldMaxExcecutionTime = Tinebase_Core::setExecutionLifeTime(300); // 5 minutes
74         
75         $account = Felamimail_Controller_Account::getInstance()->get($_message->account_id);
76         
77         try {
78             $this->_resolveOriginalMessage($_message);
79             $mail = $this->createMailForSending($_message, $account, $nonPrivateRecipients);
80             $this->_sendMailViaTransport($mail, $account, $_message, true, $nonPrivateRecipients);
81         } catch (Exception $e) {
82             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not send message: ' . $e);
83             $translation = Tinebase_Translation::getTranslation('Felamimail');
84             if (preg_match('/^501 5\.1\.3/', $e->getMessage())) {
85                 $messageText = $translation->_('Bad recipient address syntax');
86             } else if (preg_match('/^550 5\.1\.1 <(.*?)>/', $e->getMessage(), $match)) {
87                 $messageText = '<' . $match[1] . '>: ' . $translation->_('Recipient address rejected');
88             } else {
89                 $messageText = $e->getMessage();
90             }
91             $tesg = $this->_getErrorException($messageText);
92             throw $tesg;
93         }
94         
95         // reset max execution time to old value
96         Tinebase_Core::setExecutionLifeTime($oldMaxExcecutionTime);
97         
98         return $_message;
99     }
100     
101     /**
102      * places a Felamimail_Model_Message in original_id field of given message (if it had an original_id set)
103      * 
104      * @param Felamimail_Model_Message $_message
105      */
106     protected function _resolveOriginalMessage(Felamimail_Model_Message $_message)
107     {
108         if (! $_message->original_id || $_message->original_id instanceof Felamimail_Model_Message) {
109             return;
110         }
111         
112         $originalMessageId = $_message->original_id;
113         if (is_string($originalMessageId) && strpos($originalMessageId, '_') !== FALSE ) {
114             list($originalMessageId, $partId) = explode('_', $originalMessageId);
115         } else if (is_array($originalMessageId)) {
116             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
117                     . ' Something strange happened. original_id is an array: ' . print_r($originalMessageId, true));
118             return;
119         } else {
120             $partId = NULL;
121         }
122         
123         try {
124             $originalMessage = ($originalMessageId) ? $this->get($originalMessageId) : NULL;
125         } catch (Tinebase_Exception_NotFound $tenf) {
126             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
127                 . ' Did not find original message (' . $originalMessageId . ')');
128             $originalMessage = NULL;
129         }
130         
131         $_message->original_id      = $originalMessage;
132         $_message->original_part_id = $partId;
133     }
134     
135     /**
136      * save message in folder (target folder can be within a different account)
137      * 
138      * @param string|Felamimail_Model_Folder $_folder globalname or folder record
139      * @param Felamimail_Model_Message $_message
140      * @return Felamimail_Model_Message
141      */
142     public function saveMessageInFolder($_folder, $_message)
143     {
144         $sourceAccount = Felamimail_Controller_Account::getInstance()->get($_message->account_id);
145         
146         if (is_string($_folder) && ($_folder === $sourceAccount->templates_folder || $_folder === $sourceAccount->drafts_folder)) {
147             // make sure that system folder exists
148             $systemFolder = $_folder === $sourceAccount->templates_folder ? Felamimail_Model_Folder::FOLDER_TEMPLATES : Felamimail_Model_Folder::FOLDER_DRAFTS;
149             $folder = Felamimail_Controller_Account::getInstance()->getSystemFolder($sourceAccount, $systemFolder);
150         } else if ($_folder instanceof Felamimail_Model_Folder) {
151             $folder = $_folder;
152         } else {
153             $folder = Felamimail_Controller_Folder::getInstance()->getByBackendAndGlobalName($_message->account_id, $_folder);
154         }
155         
156         $targetAccount = ($_message->account_id == $folder->account_id) ? $sourceAccount : Felamimail_Controller_Account::getInstance()->get($folder->account_id);
157         
158         $mailToAppend = $this->createMailForSending($_message, $sourceAccount);
159         
160         $transport = new Felamimail_Transport();
161         $mailAsString = $transport->getRawMessage($mailToAppend, $this->_getAdditionalHeaders($_message));
162         $flags = ($folder->globalname === $targetAccount->drafts_folder) ? array(Zend_Mail_Storage::FLAG_DRAFT) : null;
163         
164         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
165             ' Appending message ' . $_message->subject . ' to folder ' . $folder->globalname . ' in account ' . $targetAccount->name);
166         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
167             ' ' . $mailAsString);
168         
169         Felamimail_Backend_ImapFactory::factory($targetAccount)->appendMessage(
170             $mailAsString,
171             Felamimail_Model_Folder::encodeFolderName($folder->globalname),
172             $flags
173         );
174         
175         return $_message;
176     }
177     
178     /**
179      * Bcc recipients need to be added separately because they are removed by default
180      * 
181      * @param Felamimail_Model_Message $message
182      * @return array
183      */
184     protected function _getAdditionalHeaders($message)
185     {
186         $additionalHeaders = ($message && ! empty($message->bcc)) ? array('Bcc' => $message->bcc) : array();
187         return $additionalHeaders;
188     }
189     
190     /**
191      * create new mail for sending via SMTP
192      * 
193      * @param Felamimail_Model_Message $_message
194      * @param Felamimail_Model_Account $_account
195      * @param array $_nonPrivateRecipients
196      * @return Tinebase_Mail
197      */
198     public function createMailForSending(Felamimail_Model_Message $_message, Felamimail_Model_Account $_account, &$_nonPrivateRecipients = array())
199     {
200         // create new mail to send
201         $mail = new Tinebase_Mail('UTF-8');
202         $mail->setSubject($_message->subject);
203         
204         $this->_setMailFrom($mail, $_account, $_message);
205         $_nonPrivateRecipients = $this->_setMailRecipients($mail, $_message);
206
207         $this->_setMailHeaders($mail, $_account, $_message);
208         $this->_addAttachments($mail, $_message);
209         $this->_setMailBody($mail, $_message);
210
211         return $mail;
212     }
213     
214     /**
215      * send mail via transport (smtp)
216      * 
217      * @param Zend_Mail $_mail
218      * @param Felamimail_Model_Account $_account
219      * @param boolean $_saveInSent
220      * @param Felamimail_Model_Message $_message
221      * @param array $_nonPrivateRecipients
222      */
223     protected function _sendMailViaTransport(Zend_Mail $_mail, Felamimail_Model_Account $_account, Felamimail_Model_Message $_message = null, $_saveInSent = false, $_nonPrivateRecipients = array())
224     {
225         $smtpConfig = $_account->getSmtpConfig();
226         if (! empty($smtpConfig) && (isset($smtpConfig['hostname']) || array_key_exists('hostname', $smtpConfig))) {
227             $transport = new Felamimail_Transport($smtpConfig['hostname'], $smtpConfig);
228             
229             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
230                 $debugConfig = $smtpConfig;
231                 $whiteList = array('hostname', 'username', 'port', 'auth', 'ssl');
232                 foreach ($debugConfig as $key => $value) {
233                     if (! in_array($key, $whiteList)) {
234                         unset($debugConfig[$key]);
235                     }
236                 }
237                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
238                     . ' About to send message via SMTP with the following config: ' . print_r($debugConfig, true));
239             }
240             
241             Tinebase_Smtp::getInstance()->sendMessage($_mail, $transport);
242             
243             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
244                 . ' successful.');
245             
246             // append mail to sent folder
247             if ($_saveInSent) {
248                 $this->_saveInSent($transport, $_account, $this->_getAdditionalHeaders($_message));
249             }
250             
251             if ($_message !== null) {
252                 // add reply/forward flags if set
253                 if (! empty($_message->flags) 
254                     && ($_message->flags == Zend_Mail_Storage::FLAG_ANSWERED || $_message->flags == Zend_Mail_Storage::FLAG_PASSED)
255                     && $_message->original_id instanceof Felamimail_Model_Message
256                 ) {
257                     Felamimail_Controller_Message_Flags::getInstance()->addFlags($_message->original_id, array($_message->flags));
258                 }
259     
260                 // add email notes to contacts (only to/cc)
261                 if ($_message->note) {
262                     $this->_addEmailNote($_nonPrivateRecipients, $_message->subject, $_message->getPlainTextBody());
263                 }
264             }
265         } else {
266             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not send message, no smtp config found.');
267         }
268     }
269     
270     /**
271      * add email notes to contacts with email addresses in $_recipients
272      *
273      * @param array $_recipients
274      * @param string $_subject
275      * 
276      * @todo add email home (when we have OR filters)
277      * @todo add link to message in sent folder?
278      */
279     protected function _addEmailNote($_recipients, $_subject, $_body)
280     {
281         $filter = new Addressbook_Model_ContactFilter(array(
282             array('field' => 'email', 'operator' => 'in', 'value' => $_recipients)
283             // OR: array('field' => 'email_home', 'operator' => 'in', 'value' => $_recipients)
284         ));
285         $contacts = Addressbook_Controller_Contact::getInstance()->search($filter);
286         
287         if (count($contacts)) {
288         
289             $translate = Tinebase_Translation::getTranslation($this->_applicationName);
290             
291             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Adding email notes to ' . count($contacts) . ' contacts.');
292             
293             $truncatedBody = (extension_loaded('mbstring')) ? mb_substr($_body, 0, 4096, 'UTF-8') : substr($_body, 0, 4096);
294             $noteText = $translate->_('Subject') . ':' . $_subject . "\n\n" . $translate->_('Body') . ': ' . $truncatedBody;
295             
296             try {
297                 foreach ($contacts as $contact) {
298                     $note = new Tinebase_Model_Note(array(
299                         'note_type_id'           => Tinebase_Notes::getInstance()->getNoteTypeByName('email')->getId(),
300                         'note'                   => $noteText,
301                         'record_id'              => $contact->getId(),
302                         'record_model'           => 'Addressbook_Model_Contact',
303                     ));
304                     
305                     Tinebase_Notes::getInstance()->addNote($note);
306                 }
307             } catch (Zend_Db_Statement_Exception $zdse) {
308                 Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Saving note failed: ' . $noteText);
309                 Tinebase_Exception::log($zdse);
310             }
311         } else {
312             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Found no contacts to add notes to.');
313         }
314     }
315     
316     /**
317      * append mail to send folder
318      * 
319      * @param Felamimail_Transport $_transport
320      * @param Felamimail_Model_Account $_account
321      * @param array $_additionalHeaders
322      * @return void
323      */
324     protected function _saveInSent(Felamimail_Transport $_transport, Felamimail_Model_Account $_account, $_additionalHeaders = array())
325     {
326         try {
327             $mailAsString = $_transport->getRawMessage(NULL, $_additionalHeaders);
328             $sentFolder = Felamimail_Controller_Account::getInstance()->getSystemFolder($_account, Felamimail_Model_Folder::FOLDER_SENT);
329             
330             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
331                 ' About to save message in sent folder (' . $sentFolder->globalname . ') ...');
332             
333             Felamimail_Backend_ImapFactory::factory($_account)->appendMessage(
334                 $mailAsString,
335                 Felamimail_Model_Folder::encodeFolderName($sentFolder->globalname)
336             );
337             
338             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
339                 . ' Saved sent message in "' . $sentFolder->globalname . '".'
340             );
341         } catch (Zend_Mail_Protocol_Exception $zmpe) {
342             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
343                 . ' Could not save sent message in "' . $sentFolder->globalname . '".'
344                 . ' Please check if a folder with this name exists.'
345                 . '(' . $zmpe->getMessage() . ')'
346             );
347         } catch (Zend_Mail_Storage_Exception $zmse) {
348             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
349                 . ' Could not save sent message in "' . $sentFolder->globalname . '".'
350                 . ' Please check if a folder with this name exists.'
351                 . '(' . $zmse->getMessage() . ')'
352             );
353         }
354     }
355     
356     /**
357      * send Zend_Mail message via smtp
358      * 
359      * @param  mixed      $accountId
360      * @param  Zend_Mail  $mail
361      * @param  boolean    $saveInSent
362      * @param  Felamimail_Model_Message $originalMessage
363      * @return Zend_Mail
364      */
365     public function sendZendMail($accountId, Zend_Mail $mail, $saveInSent = false, $originalMessage = NULL)
366     {
367         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
368             ' Sending message with subject "' . $mail->getSubject() . '" to ' . print_r($mail->getRecipients(), TRUE));
369         if ($originalMessage !== NULL) {
370             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
371                 ' Original Message subject: ' . $originalMessage->subject . ' / Flag to set: ' . var_export($originalMessage->flags, TRUE)
372             );
373             
374             // this is required for adding the reply/forward flag in _sendMailViaTransport()
375             $originalMessage->original_id = $originalMessage;
376         }
377         
378         // increase execution time (sending message with attachments can take a long time)
379         $oldMaxExcecutionTime = Tinebase_Core::setExecutionLifeTime(300); // 5 minutes
380         
381         // get account
382         $account = ($accountId instanceof Felamimail_Model_Account) ? $accountId : Felamimail_Controller_Account::getInstance()->get($accountId);
383         
384         $this->_setMailFrom($mail, $account);
385         $this->_setMailHeaders($mail, $account);
386         $this->_sendMailViaTransport($mail, $account, $originalMessage, $saveInSent);
387         
388         // reset max execution time to old value
389         Tinebase_Core::setExecutionLifeTime($oldMaxExcecutionTime);
390         
391         return $mail;
392     }
393     
394     /**
395      * set mail body
396      * 
397      * @param Tinebase_Mail $_mail
398      * @param Felamimail_Model_Message $_message
399      */
400     protected function _setMailBody(Tinebase_Mail $_mail, Felamimail_Model_Message $_message)
401     {
402         if (strpos($_message->body, '-----BEGIN PGP MESSAGE-----') === 0) {
403             $_mail->setBodyPGPMime($_message->body);
404             return;
405         }
406
407         if ($_message->content_type == Felamimail_Model_Message::CONTENT_TYPE_HTML) {
408             $_mail->setBodyHtml(Felamimail_Message::addHtmlMarkup($_message->body));
409             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $_mail->getBodyHtml(TRUE));
410         }
411         
412         $plainBodyText = $_message->getPlainTextBody();
413         $_mail->setBodyText($plainBodyText);
414         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $_mail->getBodyText(TRUE));
415     }
416     
417     /**
418      * set from in mail to be sent
419      * 
420      * @param Tinebase_Mail $_mail
421      * @param Felamimail_Model_Account $_account
422      * @param Felamimail_Model_Message $_message
423      */
424     protected function _setMailFrom(Zend_Mail $_mail, Felamimail_Model_Account $_account, Felamimail_Model_Message $_message = NULL)
425     {
426         $_mail->clearFrom();
427         
428         $from = $this->_getSenderName($_message, $_account);
429         
430         $email = ($_message !== NULL && ! empty($_message->from_email)) ? $_message->from_email : $_account->email;
431         
432         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Set from for mail: ' . $email . ' / ' . $from);
433         
434         $_mail->setFrom($email, $from);
435     }
436
437     /**
438      * @param $_message
439      * @param $_account
440      * @return string
441      */
442     protected function _getSenderName($_message, $_account)
443     {
444         $messageFrom = ($_message && ! empty($_message->from_name)) ? $_message->from_name : null;
445
446         return $messageFrom
447             ? $messageFrom
448             : (isset($_account->from) && ! empty($_account->from)
449                 ? $_account->from
450                 : Tinebase_Core::getUser()->accountFullName);
451     }
452     
453     /**
454      * set mail recipients
455      * 
456      * @param Tinebase_Mail $_mail
457      * @param Felamimail_Model_Message $_message
458      * @return array
459      */
460     protected function _setMailRecipients(Zend_Mail $_mail, Felamimail_Model_Message $_message)
461     {
462         $nonPrivateRecipients = array();
463         $punycodeConverter = $this->getPunycodeConverter();
464         $invalidEmailAddresses = array();
465         
466         foreach (array('to', 'cc', 'bcc') as $type) {
467             if (isset($_message->{$type})) {
468                 foreach((array) $_message->{$type} as $address) {
469
470                     $punyCodedAddress = $punycodeConverter->encode($address);
471
472                     if (! preg_match(Tinebase_Mail::EMAIL_ADDRESS_REGEXP, $punyCodedAddress)) {
473                         $invalidEmailAddresses[] = $address;
474                         continue;
475                     }
476
477                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::'
478                         . __LINE__ . ' Add ' . $type . ' address: ' . $punyCodedAddress);
479                     
480                     switch($type) {
481                         case 'to':
482                             $_mail->addTo($punyCodedAddress);
483                             $nonPrivateRecipients[] = $punyCodedAddress;
484                             break;
485                         case 'cc':
486                             $_mail->addCc($punyCodedAddress);
487                             $nonPrivateRecipients[] = $punyCodedAddress;
488                             break;
489                         case 'bcc':
490                             $_mail->addBcc($punyCodedAddress);
491                             break;
492                     }
493                 }
494             }
495         }
496
497         if (count($invalidEmailAddresses) > 0) {
498             $translation = Tinebase_Translation::getTranslation('Felamimail');
499             $messageText = '<' . implode(',', $invalidEmailAddresses) . '>: ' . $translation->_('Invalid address format');
500             $fe = new Felamimail_Exception($messageText);
501             throw $fe;
502         }
503         
504         return $nonPrivateRecipients;
505     }
506
507     protected function _getErrorException($messageText)
508     {
509         $translation = Tinebase_Translation::getTranslation('Felamimail');
510         $message = sprintf($translation->_('Error: %s'), $messageText);
511         $tesg = new Tinebase_Exception_SystemGeneric($message);
512         $tesg->setTitle($translation->_('Could not send message'));
513
514         return $tesg;
515     }
516     
517     /**
518      * set headers in mail to be sent
519      * 
520      * @param Tinebase_Mail $_mail
521      * @param Felamimail_Model_Account $_account
522      * @param Felamimail_Model_Message $_message
523      */
524     protected function _setMailHeaders(Zend_Mail $_mail, Felamimail_Model_Account $_account, Felamimail_Model_Message $_message = NULL)
525     {
526         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Setting mail headers');
527         
528         // add user agent
529         $_mail->addHeader('User-Agent', Tinebase_Core::getTineUserAgent('Email Client'));
530         
531         // set organization
532         if (isset($_account->organization) && ! empty($_account->organization)) {
533             $_mail->addHeader('Organization', $_account->organization);
534         }
535
536         // add reply-to
537         if (! empty($_account->reply_to)) {
538             $_mail->setReplyTo($_account->reply_to, $this->_getSenderName($_message, $_account));
539         }
540         
541         // set message-id (we could use Zend_Mail::createMessageId() here)
542         if ($_mail->getMessageId() === NULL) {
543             $domainPart = substr($_account->email, strpos($_account->email, '@'));
544             $uid = Tinebase_Record_Abstract::generateUID();
545             $_mail->setMessageId($uid . $domainPart);
546         }
547         
548         if ($_message !== NULL) {
549             if ($_message->flags && $_message->flags == Zend_Mail_Storage::FLAG_ANSWERED && $_message->original_id instanceof Felamimail_Model_Message) {
550                 $this->_addReplyHeaders($_message);
551             }
552             
553             // set the header request response
554             if ($_message->reading_conf) {
555                 $_mail->addHeader('Disposition-Notification-To', $_message->from_email);
556             }
557             
558             // add other headers
559             if (! empty($_message->headers) && is_array($_message->headers)) {
560                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
561                     . ' Adding custom headers: ' . print_r($_message->headers, TRUE));
562                 foreach ($_message->headers as $key => $value) {
563                     $value = $this->_trimHeader($key, $value);
564                     $_mail->addHeader($key, $value);
565                 }
566             }
567         }
568     }
569     
570     /**
571      * trim message headers (Zend_Mail only supports < 998 chars)
572      * 
573      * @param string $value
574      * @return string
575      */
576     protected function _trimHeader($key, $value)
577     {
578         if (strlen($value) + strlen($key) > 998) {
579             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
580                 . ' Trimming header ' . $key);
581             
582             $value = substr(trim($value), 0, (995 - strlen($key)));
583
584             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
585                 . $value);
586         }
587         
588         return $value;
589     }
590     
591     /**
592      * set In-Reply-To and References headers
593      * 
594      * @param Felamimail_Model_Message $message
595      * 
596      * @see http://www.faqs.org/rfcs/rfc2822.html / Section 3.6.4.
597      */
598     protected function _addReplyHeaders(Felamimail_Model_Message $message)
599     {
600         $originalHeaders = Felamimail_Controller_Message::getInstance()->getMessageHeaders($message->original_id);
601         if (! isset($originalHeaders['message-id'])) {
602             // no message-id -> skip this
603             return;
604         }
605
606         $messageHeaders = is_array($message->headers) ? $message->headers : array();
607         $messageHeaders['In-Reply-To'] = $originalHeaders['message-id'];
608         
609         $references = '';
610         if (isset($originalHeaders['references'])) {
611             $references = $originalHeaders['references'] . ' ';
612         } else if (isset($originalHeaders['in-reply-to'])) {
613             $references = $originalHeaders['in-reply-to'] . ' ';
614         }
615         $references .= $originalHeaders['message-id'];
616         $messageHeaders['References'] = $references;
617         
618         $message->headers = $messageHeaders;
619     }
620     
621     /**
622      * add attachments to mail
623      *
624      * @param Tinebase_Mail $_mail
625      * @param Felamimail_Model_Message $_message
626      * @throws Felamimail_Exception_IMAP
627      */
628     protected function _addAttachments(Tinebase_Mail $_mail, Felamimail_Model_Message $_message)
629     {
630         if (! isset($_message->attachments) || empty($_message->attachments)) {
631             return;
632         }
633
634         $maxAttachmentSize = $this->_getMaxAttachmentSize();
635         $totalSize = 0;
636
637         foreach ($_message->attachments as $attachment) {
638             $part = $this->_getAttachmentPartByType($attachment, $_message);
639
640             if (! $part || ! isset($attachment['type'])) {
641                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
642                     . ' Skipping attachment ' . print_r($attachment, true));
643                 continue;
644             }
645
646             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
647                 . ' Adding attachment: ' . (is_object($attachment) ? print_r($attachment->toArray(), TRUE) : print_r($attachment, TRUE)));
648
649             $part->setTypeAndDispositionForAttachment($attachment['type'], $attachment['name']);
650
651             if (! empty($attachment['size'])) {
652                 $totalSize += $attachment['size'];
653             }
654             
655             if ($totalSize > $maxAttachmentSize) {
656                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
657                     . ' Current attachment size: ' . Tinebase_Helper::convertToMegabytes($totalSize) . ' MB / allowed size: '
658                     . Tinebase_Helper::convertToMegabytes($maxAttachmentSize) . ' MB');
659                 throw new Felamimail_Exception_IMAP('Maximum attachment size exceeded. Please remove one or more attachments.');
660             }
661             
662             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
663                 . ' Adding attachment ' . $part->type);
664             
665             $_mail->addAttachment($part);
666         }
667     }
668
669     /**
670      * @param $attachment
671      * @return null|Zend_Mime_Part
672      */
673     protected function _getAttachmentPartByType(&$attachment, $_message)
674     {
675         $part = null;
676
677         $attachmentType = $this->_getAttachmentType($attachment, $_message);
678
679         switch ($attachmentType) {
680             case 'rfc822':
681                 $part = $this->_getRfc822Attachment($attachment, $_message);
682                 break;
683             case 'systemlink_fm':
684                 $this->_setSystemlinkAttachment($attachment, $_message);
685                 break;
686             case 'download_public':
687             case 'download_public_fm':
688                 // no attachment part
689                 $this->_setDownloadLinkAttachment($attachment, $_message);
690                 break;
691             case 'download_protected':
692             case 'download_protected_fm':
693                 // no attachment part
694                 $this->_setDownloadLinkAttachment($attachment, $_message, /* protected */ true);
695                 break;
696             case 'filenode':
697                 $part = $this->_getFileNodeAttachment($attachment);
698                 break;
699             case 'tempfile':
700                 $part = $this->_getTempFileAttachment($attachment);
701                 break;
702             default:
703                 $part = $this->_getMessagePartAttachment($attachment);
704         }
705
706         return $part;
707     }
708
709     protected function _getAttachmentType($attachment, $_message)
710     {
711         // Determine if it's a tempfile attachment or a filenode attachment
712         if (isset($attachment['attachment_type']) && $attachment['attachment_type'] === 'attachment' && $attachment['tempFile']) {
713             $attachment['attachment_type'] = 'tempfile';
714         }
715
716         if (isset($attachment['attachment_type']) && $attachment['attachment_type'] === 'attachment' && !$attachment['tempFile']) {
717             $attachment['attachment_type'] = 'filenode';
718         }
719
720         if (isset($attachment['attachment_type'])) {
721             return $attachment['attachment_type'];
722         } elseif (isset($attachment['type'])
723             && $attachment['type'] === Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822
724             && $_message->original_id instanceof Felamimail_Model_Message
725         ) {
726             return 'rfc822';
727         } elseif ($attachment instanceof Tinebase_Model_TempFile || isset($attachment['tempFile'])) {
728             return 'tempfile';
729         }
730
731         return null;
732     }
733
734     /**
735      * get attachment of type CONTENT_TYPE_MESSAGE_RFC822
736      *
737      * @param $attachment
738      * @param $message
739      * @return Zend_Mime_Part
740      */
741     protected function _getRfc822Attachment(&$attachment, $message)
742     {
743         $part = $this->getMessagePart($message->original_id, ($message->original_part_id) ? $message->original_part_id : NULL);
744         $part->decodeContent();
745
746         // replace some chars from attachment name
747         $attachment['name'] = preg_replace("/[\s'\"]*/", "", $attachment['name']) . '.eml';
748
749         return $part;
750     }
751
752     /**
753      * @param            $_attachment
754      * @param            $_message
755      * @param bool|false $_protected
756      * @return boolean success
757      */
758     protected function _setDownloadLinkAttachment($_attachment, $_message, $_protected = false)
759     {
760         if (! Tinebase_Core::getUser()->hasRight('Filemanager', Tinebase_Acl_Rights::RUN)) {
761             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
762                 . ' No right to run Filemanager');
763             return false;
764         }
765
766         $password = $_protected && isset($_attachment['password']) ? $_attachment['password'] : '';
767         $tempFile = $this->_getTempFileFromAttachment($_attachment);
768         if ($tempFile) {
769             $translate = Tinebase_Translation::getTranslation('Felamimail');
770             $downloadLinkFolder = '/' . Tinebase_FileSystem::FOLDER_TYPE_PERSONAL
771                 . '/' . Tinebase_Core::getUser()->getId()
772                 . '/' . $translate->_('.My Mail Download Links');
773             $downloadLink = Filemanager_Controller_Node::getInstance()->createNodeWithDownloadLinkFromTempFile(
774                 $tempFile,
775                 $downloadLinkFolder,
776                 $password
777             );
778         } else {
779             $node = Filemanager_Controller_Node::getInstance()->get($_attachment['id']);
780
781             if (!Tinebase_Core::getUser()->hasGrant($node, Tinebase_Model_Grants::GRANT_PUBLISH)) {
782                 return false;
783             }
784
785             $downloadLink = Filemanager_Controller_DownloadLink::getInstance()->create(new Filemanager_Model_DownloadLink(array(
786                 'node_id'       => $node->getId(),
787                 'expiry_date'   => Tinebase_DateTime::now()->addDay(30)->toString(),
788                 'password'      => $password
789             )));
790         }
791
792         $this->_insertDownloadLinkIntoMailBody($downloadLink->url, $_message);
793
794         return true;
795     }
796
797     /**
798      * @param $_attachment
799      * @param $_message
800      * @return bool
801      */
802     protected function _setSystemlinkAttachment($_attachment, $_message)
803     {
804         if (! Tinebase_Core::getUser()->hasRight('Filemanager', Tinebase_Acl_Rights::RUN)) {
805             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
806                 . ' No right to run Filemanager');
807             return false;
808         }
809
810         $node = Filemanager_Controller_Node::getInstance()->get($_attachment['id']);
811
812         $this->_insertDownloadLinkIntoMailBody(Filemanager_Model_Node::getDeepLink($node), $_message);
813
814         return true;
815     }
816
817     /**
818      * @param $_link
819      * @param $_message
820      *
821      * TODO insert above signature
822      */
823     protected function _insertDownloadLinkIntoMailBody($_link, $_message)
824     {
825         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
826             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
827                 . ' Inserting download link into mail body: ' . $_link);
828         }
829
830         if ('text/html' === $_message->content_type) {
831             $_message->body .= sprintf(
832                 '<br />%s<br />',
833                 $_link
834             );
835         } else {
836             $_message->body .= "\n" . $_link . "\n";
837         }
838     }
839
840     /**
841      * get attachment defined by a file node (mailfiler or filemanager)
842      *
843      * @param $attachment
844      * @return null|Zend_Mime_Part
845      * @throws Tinebase_Exception_NotFound
846      *
847      */
848     protected function _getFileNodeAttachment(&$attachment)
849     {
850         if (isset($attachment['path'])) {
851             // allow Filemanager?
852             $appname = 'Filemanager';
853             $path = $attachment['path'];
854         } else {
855             list($appname, $path, $messageuid, $partId) = explode('|', $attachment['id']);
856         }
857
858         try {
859             $nodeController = Tinebase_Core::getApplicationInstance($appname . '_Model_Node');
860         } catch (Tinebase_Exception $te) {
861             Tinebase_Exception::log($te);
862             return null;
863         }
864
865         // remove filename from path
866         // TODO remove DRY with \MailFiler_Frontend_Http::downloadAttachment
867         $pathParts = explode('/', $path);
868         array_pop($pathParts);
869         $path = implode('/', $pathParts);
870
871         if ($appname === 'MailFiler') {
872             $filter = array(
873                 array(
874                     'field' => 'path',
875                     'operator' => 'equals',
876                     'value' => $path
877                 ),
878                 array(
879                     'field' => 'messageuid',
880                     'operator' => 'equals',
881                     'value' => $messageuid
882                 )
883             );
884             $node = $nodeController->search(new MailFiler_Model_NodeFilter($filter))->getFirstRecord();
885         } else {
886             $nodeController = Filemanager_Controller_Node::getInstance();
887             $node = $nodeController->get($attachment['id']);
888
889             if (!Tinebase_Core::getUser()->hasGrant($node, Tinebase_Model_Grants::GRANT_DOWNLOAD)) {
890                 return null;
891             }
892
893             $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath(
894                 Filemanager_Controller_Node::getInstance()->addBasePath($node->path)
895             );
896         }
897
898         if ($node) {
899             if ($appname === 'MailFiler') {
900                 $mailpart = MailFiler_Controller_Message::getInstance()->getPartFromNode($node, $partId);
901                 // TODO use stream
902                 $content = Felamimail_Message::getDecodedContent($mailpart);
903
904             } elseif ($appname === 'Filemanager') {
905                 $content = fopen($pathRecord->streamwrapperpath, 'r');
906
907             } else {
908                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
909                     . ' We don\'t support ' . $appname . ' nodes as attachment yet.');
910             }
911
912             $part = new Zend_Mime_Part($content);
913             $part->encoding = Zend_Mime::ENCODING_BASE64;
914
915         } else {
916             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
917                 . ' Could not find file node attachment');
918             $part = null;
919         }
920
921         return $part;
922     }
923
924     /**
925      * get attachment defined by temp file
926      *
927      * @param $attachment
928      * @return null|Zend_Mime_Part
929      * @throws Tinebase_Exception_NotFound
930      */
931     protected function _getTempFileAttachment(&$attachment)
932     {
933         $tempFile = $this->_getTempFileFromAttachment($attachment);
934         if ($tempFile === null) {
935             return null;
936         }
937
938         if (! $tempFile->path) {
939             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Could not find attachment.');
940             return null;
941         }
942
943         // get contents from uploaded file
944         $stream = fopen($tempFile->path, 'r');
945         $part = new Zend_Mime_Part($stream);
946
947         // RFC822 attachments are not encoded, set all others to ENCODING_BASE64
948         $part->encoding = ($tempFile->type == Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822) ? null : Zend_Mime::ENCODING_BASE64;
949
950         $attachment['name'] = $tempFile->name;
951         $attachment['type'] = $tempFile->type;
952
953         if (! empty($tempFile->size)) {
954             $attachment['size'] = $tempFile->size;
955         }
956
957         return $part;
958     }
959
960     /**
961      * @param $attachment
962      * @return null|Tinebase_Model_TempFile|Tinebase_Record_Interface
963      * @throws Tinebase_Exception_NotFound
964      */
965     protected function _getTempFileFromAttachment($attachment)
966     {
967         $tempFileBackend = Tinebase_TempFile::getInstance();
968         $tempFile = ($attachment instanceof Tinebase_Model_TempFile)
969             ? $attachment
970             : (((isset($attachment['tempFile']) || array_key_exists('tempFile', $attachment))) ? $tempFileBackend->get($attachment['tempFile']['id']) : NULL);
971
972         return $tempFile;
973     }
974
975     /**
976      * get attachment part defined by message id + part id
977      *
978      * @param $attachment
979      * @return null|Zend_Mime_Part
980      */
981     protected function _getMessagePartAttachment(&$attachment)
982     {
983         if (! isset($attachment['id']) || strpos($attachment['id'], '_') === false) {
984             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' No valid message id/part id');
985             return null;
986         }
987
988         // might be an attachment defined by message id + part id -> fetch this and attach
989         list($messageId, $partId) = explode('_', $attachment['id']);
990         $part = $this->getMessagePart($messageId, $partId);
991         $part->decodeContent();
992
993         return $part;
994     }
995     
996     /**
997      * get max attachment size for outgoing mails
998      * 
999      * - currently it is set to memory_limit
1000      * - returns size in Bytes
1001      * 
1002      * @return integer
1003      */
1004     protected function _getMaxAttachmentSize()
1005     {
1006         $configuredMemoryLimit = ini_get('memory_limit');
1007         
1008         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1009             . ' memory_limit = ' . $configuredMemoryLimit);
1010         
1011         if ($configuredMemoryLimit === FALSE or $configuredMemoryLimit == -1) {
1012             // set to a big default value
1013             $configuredMemoryLimit = '512M';
1014         }
1015         
1016         return Tinebase_Helper::convertToBytes($configuredMemoryLimit);
1017     }
1018 }