5592764172dd4bbda65de4697885dd5ea64da7cb
[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         try {
77             $this->_resolveOriginalMessage($_message);
78             $mail = $this->createMailForSending($_message, $account, $nonPrivateRecipients);
79             $this->_sendMailViaTransport($mail, $account, $_message, true, $nonPrivateRecipients);
80         } catch (Exception $e) {
81             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not send message: ' . $e);
82             $translation = Tinebase_Translation::getTranslation('Felamimail');
83             if (preg_match('/^501 5\.1\.3/', $e->getMessage())) {
84                 $messageText = $translation->_('Bad recipient address syntax');
85             } else if (preg_match('/^550 5\.1\.1 <(.*?)>/', $e->getMessage(), $match)) {
86                 $messageText = '<' . $match[1] . '>: ' . $translation->_('Recipient address rejected');
87             } else {
88                 $messageText = $e->getMessage();
89             }
90             $tesg = $this->_getErrorException($messageText);
91             throw $tesg;
92         }
93         
94         // reset max execution time to old value
95         Tinebase_Core::setExecutionLifeTime($oldMaxExcecutionTime);
96         
97         return $_message;
98     }
99     
100     /**
101      * places a Felamimail_Model_Message in original_id field of given message (if it had an original_id set)
102      * 
103      * @param Felamimail_Model_Message $_message
104      */
105     protected function _resolveOriginalMessage(Felamimail_Model_Message $_message)
106     {
107         if (! $_message->original_id || $_message->original_id instanceof Felamimail_Model_Message) {
108             return;
109         }
110         
111         $originalMessageId = $_message->original_id;
112         if (is_string($originalMessageId) && strpos($originalMessageId, '_') !== FALSE ) {
113             list($originalMessageId, $partId) = explode('_', $originalMessageId);
114         } else if (is_array($originalMessageId)) {
115             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
116                     . ' Something strange happened. original_id is an array: ' . print_r($originalMessageId, true));
117             return;
118         } else {
119             $partId = NULL;
120         }
121         
122         try {
123             $originalMessage = ($originalMessageId) ? $this->get($originalMessageId) : NULL;
124         } catch (Tinebase_Exception_NotFound $tenf) {
125             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
126                 . ' Did not find original message (' . $originalMessageId . ')');
127             $originalMessage = NULL;
128         }
129         
130         $_message->original_id      = $originalMessage;
131         $_message->original_part_id = $partId;
132     }
133     
134     /**
135      * save message in folder (target folder can be within a different account)
136      * 
137      * @param string|Felamimail_Model_Folder $_folder globalname or folder record
138      * @param Felamimail_Model_Message $_message
139      * @return Felamimail_Model_Message
140      */
141     public function saveMessageInFolder($_folder, $_message)
142     {
143         $sourceAccount = Felamimail_Controller_Account::getInstance()->get($_message->account_id);
144         
145         if (is_string($_folder) && ($_folder === $sourceAccount->templates_folder || $_folder === $sourceAccount->drafts_folder)) {
146             // make sure that system folder exists
147             $systemFolder = $_folder === $sourceAccount->templates_folder ? Felamimail_Model_Folder::FOLDER_TEMPLATES : Felamimail_Model_Folder::FOLDER_DRAFTS;
148             $folder = Felamimail_Controller_Account::getInstance()->getSystemFolder($sourceAccount, $systemFolder);
149         } else if ($_folder instanceof Felamimail_Model_Folder) {
150             $folder = $_folder;
151         } else {
152             $folder = Felamimail_Controller_Folder::getInstance()->getByBackendAndGlobalName($_message->account_id, $_folder);
153         }
154         
155         $targetAccount = ($_message->account_id == $folder->account_id) ? $sourceAccount : Felamimail_Controller_Account::getInstance()->get($folder->account_id);
156         
157         $mailToAppend = $this->createMailForSending($_message, $sourceAccount);
158         
159         $transport = new Felamimail_Transport();
160         $mailAsString = $transport->getRawMessage($mailToAppend, $this->_getAdditionalHeaders($_message));
161         $flags = ($folder->globalname === $targetAccount->drafts_folder) ? array(Zend_Mail_Storage::FLAG_DRAFT) : null;
162         
163         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
164             ' Appending message ' . $_message->subject . ' to folder ' . $folder->globalname . ' in account ' . $targetAccount->name);
165         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
166             ' ' . $mailAsString);
167         
168         Felamimail_Backend_ImapFactory::factory($targetAccount)->appendMessage(
169             $mailAsString,
170             Felamimail_Model_Folder::encodeFolderName($folder->globalname),
171             $flags
172         );
173         
174         return $_message;
175     }
176     
177     /**
178      * Bcc recipients need to be added separately because they are removed by default
179      * 
180      * @param Felamimail_Model_Message $message
181      * @return array
182      */
183     protected function _getAdditionalHeaders($message)
184     {
185         $additionalHeaders = ($message && ! empty($message->bcc)) ? array('Bcc' => $message->bcc) : array();
186         return $additionalHeaders;
187     }
188     
189     /**
190      * create new mail for sending via SMTP
191      * 
192      * @param Felamimail_Model_Message $_message
193      * @param Felamimail_Model_Account $_account
194      * @param array $_nonPrivateRecipients
195      * @return Tinebase_Mail
196      */
197     public function createMailForSending(Felamimail_Model_Message $_message, Felamimail_Model_Account $_account, &$_nonPrivateRecipients = array())
198     {
199         // create new mail to send
200         $mail = new Tinebase_Mail('UTF-8');
201         $mail->setSubject($_message->subject);
202         
203         $this->_setMailBody($mail, $_message);
204         $this->_setMailFrom($mail, $_account, $_message);
205         $_nonPrivateRecipients = $this->_setMailRecipients($mail, $_message);
206         $this->_setMailHeaders($mail, $_account, $_message);
207         
208         $this->_addAttachments($mail, $_message);
209         
210         return $mail;
211     }
212     
213     /**
214      * send mail via transport (smtp)
215      * 
216      * @param Zend_Mail $_mail
217      * @param Felamimail_Model_Account $_account
218      * @param boolean $_saveInSent
219      * @param Felamimail_Model_Message $_message
220      * @param array $_nonPrivateRecipients
221      */
222     protected function _sendMailViaTransport(Zend_Mail $_mail, Felamimail_Model_Account $_account, Felamimail_Model_Message $_message = null, $_saveInSent = false, $_nonPrivateRecipients = array())
223     {
224         $smtpConfig = $_account->getSmtpConfig();
225         if (! empty($smtpConfig) && (isset($smtpConfig['hostname']) || array_key_exists('hostname', $smtpConfig))) {
226             $transport = new Felamimail_Transport($smtpConfig['hostname'], $smtpConfig);
227             
228             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
229                 $debugConfig = $smtpConfig;
230                 $whiteList = array('hostname', 'username', 'port', 'auth', 'ssl');
231                 foreach ($debugConfig as $key => $value) {
232                     if (! in_array($key, $whiteList)) {
233                         unset($debugConfig[$key]);
234                     }
235                 }
236                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
237                     . ' About to send message via SMTP with the following config: ' . print_r($debugConfig, true));
238             }
239             
240             Tinebase_Smtp::getInstance()->sendMessage($_mail, $transport);
241             
242             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
243                 . ' successful.');
244             
245             // append mail to sent folder
246             if ($_saveInSent) {
247                 $this->_saveInSent($transport, $_account, $this->_getAdditionalHeaders($_message));
248             }
249             
250             if ($_message !== null) {
251                 // add reply/forward flags if set
252                 if (! empty($_message->flags) 
253                     && ($_message->flags == Zend_Mail_Storage::FLAG_ANSWERED || $_message->flags == Zend_Mail_Storage::FLAG_PASSED)
254                     && $_message->original_id instanceof Felamimail_Model_Message
255                 ) {
256                     Felamimail_Controller_Message_Flags::getInstance()->addFlags($_message->original_id, array($_message->flags));
257                 }
258     
259                 // add email notes to contacts (only to/cc)
260                 if ($_message->note) {
261                     $this->_addEmailNote($_nonPrivateRecipients, $_message->subject, $_message->getPlainTextBody());
262                 }
263             }
264         } else {
265             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not send message, no smtp config found.');
266         }
267     }
268     
269     /**
270      * add email notes to contacts with email addresses in $_recipients
271      *
272      * @param array $_recipients
273      * @param string $_subject
274      * 
275      * @todo add email home (when we have OR filters)
276      * @todo add link to message in sent folder?
277      */
278     protected function _addEmailNote($_recipients, $_subject, $_body)
279     {
280         $filter = new Addressbook_Model_ContactFilter(array(
281             array('field' => 'email', 'operator' => 'in', 'value' => $_recipients)
282             // OR: array('field' => 'email_home', 'operator' => 'in', 'value' => $_recipients)
283         ));
284         $contacts = Addressbook_Controller_Contact::getInstance()->search($filter);
285         
286         if (count($contacts)) {
287         
288             $translate = Tinebase_Translation::getTranslation($this->_applicationName);
289             
290             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Adding email notes to ' . count($contacts) . ' contacts.');
291             
292             $truncatedBody = (extension_loaded('mbstring')) ? mb_substr($_body, 0, 4096, 'UTF-8') : substr($_body, 0, 4096);
293             $noteText = $translate->_('Subject') . ':' . $_subject . "\n\n" . $translate->_('Body') . ': ' . $truncatedBody;
294             
295             try {
296                 foreach ($contacts as $contact) {
297                     $note = new Tinebase_Model_Note(array(
298                         'note_type_id'           => Tinebase_Notes::getInstance()->getNoteTypeByName('email')->getId(),
299                         'note'                   => $noteText,
300                         'record_id'              => $contact->getId(),
301                         'record_model'           => 'Addressbook_Model_Contact',
302                     ));
303                     
304                     Tinebase_Notes::getInstance()->addNote($note);
305                 }
306             } catch (Zend_Db_Statement_Exception $zdse) {
307                 Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Saving note failed: ' . $noteText);
308                 Tinebase_Exception::log($zdse);
309             }
310         } else {
311             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Found no contacts to add notes to.');
312         }
313     }
314     
315     /**
316      * append mail to send folder
317      * 
318      * @param Felamimail_Transport $_transport
319      * @param Felamimail_Model_Account $_account
320      * @param array $_additionalHeaders
321      * @return void
322      */
323     protected function _saveInSent(Felamimail_Transport $_transport, Felamimail_Model_Account $_account, $_additionalHeaders = array())
324     {
325         try {
326             $mailAsString = $_transport->getRawMessage(NULL, $_additionalHeaders);
327             $sentFolder = Felamimail_Controller_Account::getInstance()->getSystemFolder($_account, Felamimail_Model_Folder::FOLDER_SENT);
328             
329             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
330                 ' About to save message in sent folder (' . $sentFolder->globalname . ') ...');
331             
332             Felamimail_Backend_ImapFactory::factory($_account)->appendMessage(
333                 $mailAsString,
334                 Felamimail_Model_Folder::encodeFolderName($sentFolder->globalname)
335             );
336             
337             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
338                 . ' Saved sent message in "' . $sentFolder->globalname . '".'
339             );
340         } catch (Zend_Mail_Protocol_Exception $zmpe) {
341             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
342                 . ' Could not save sent message in "' . $sentFolder->globalname . '".'
343                 . ' Please check if a folder with this name exists.'
344                 . '(' . $zmpe->getMessage() . ')'
345             );
346         } catch (Zend_Mail_Storage_Exception $zmse) {
347             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
348                 . ' Could not save sent message in "' . $sentFolder->globalname . '".'
349                 . ' Please check if a folder with this name exists.'
350                 . '(' . $zmse->getMessage() . ')'
351             );
352         }
353     }
354     
355     /**
356      * send Zend_Mail message via smtp
357      * 
358      * @param  mixed      $accountId
359      * @param  Zend_Mail  $mail
360      * @param  boolean    $saveInSent
361      * @param  Felamimail_Model_Message $originalMessage
362      * @return Zend_Mail
363      */
364     public function sendZendMail($accountId, Zend_Mail $mail, $saveInSent = false, $originalMessage = NULL)
365     {
366         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
367             ' Sending message with subject "' . $mail->getSubject() . '" to ' . print_r($mail->getRecipients(), TRUE));
368         if ($originalMessage !== NULL) {
369             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
370                 ' Original Message subject: ' . $originalMessage->subject . ' / Flag to set: ' . var_export($originalMessage->flags, TRUE)
371             );
372             
373             // this is required for adding the reply/forward flag in _sendMailViaTransport()
374             $originalMessage->original_id = $originalMessage;
375         }
376         
377         // increase execution time (sending message with attachments can take a long time)
378         $oldMaxExcecutionTime = Tinebase_Core::setExecutionLifeTime(300); // 5 minutes
379         
380         // get account
381         $account = ($accountId instanceof Felamimail_Model_Account) ? $accountId : Felamimail_Controller_Account::getInstance()->get($accountId);
382         
383         $this->_setMailFrom($mail, $account);
384         $this->_setMailHeaders($mail, $account);
385         $this->_sendMailViaTransport($mail, $account, $originalMessage, $saveInSent);
386         
387         // reset max execution time to old value
388         Tinebase_Core::setExecutionLifeTime($oldMaxExcecutionTime);
389         
390         return $mail;
391     }
392     
393     /**
394      * set mail body
395      * 
396      * @param Tinebase_Mail $_mail
397      * @param Felamimail_Model_Message $_message
398      */
399     protected function _setMailBody(Tinebase_Mail $_mail, Felamimail_Model_Message $_message)
400     {
401         if (strpos($_message->body, '-----BEGIN PGP MESSAGE-----') === 0) {
402             $_mail->setBodyPGPMime($_message->body);
403             return;
404         }
405
406         if ($_message->content_type == Felamimail_Model_Message::CONTENT_TYPE_HTML) {
407             $_mail->setBodyHtml(Felamimail_Message::addHtmlMarkup($_message->body));
408             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $_mail->getBodyHtml(TRUE));
409         }
410         
411         $plainBodyText = $_message->getPlainTextBody();
412         $_mail->setBodyText($plainBodyText);
413         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $_mail->getBodyText(TRUE));
414     }
415     
416     /**
417      * set from in mail to be sent
418      * 
419      * @param Tinebase_Mail $_mail
420      * @param Felamimail_Model_Account $_account
421      * @param Felamimail_Model_Message $_message
422      */
423     protected function _setMailFrom(Zend_Mail $_mail, Felamimail_Model_Account $_account, Felamimail_Model_Message $_message = NULL)
424     {
425         $_mail->clearFrom();
426         
427         $from = $this->_getSenderName($_account);
428         
429         $email = ($_message !== NULL && ! empty($_message->from_email)) ? $_message->from_email : $_account->email;
430         
431         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Set from for mail: ' . $email . ' / ' . $from);
432         
433         $_mail->setFrom($email, $from);
434     }
435
436     protected function _getSenderName($account)
437     {
438         return (isset($_account->from) && ! empty($_account->from))
439             ? $_account->from
440             : Tinebase_Core::getUser()->accountFullName;
441     }
442     
443     /**
444      * set mail recipients
445      * 
446      * @param Tinebase_Mail $_mail
447      * @param Felamimail_Model_Message $_message
448      * @return array
449      */
450     protected function _setMailRecipients(Zend_Mail $_mail, Felamimail_Model_Message $_message)
451     {
452         $nonPrivateRecipients = array();
453         $punycodeConverter = $this->getPunycodeConverter();
454         $invalidEmailAddresses = array();
455         
456         foreach (array('to', 'cc', 'bcc') as $type) {
457             if (isset($_message->{$type})) {
458                 foreach((array) $_message->{$type} as $address) {
459
460                     $punyCodedAddress = $punycodeConverter->encode($address);
461
462                     if (! preg_match(Tinebase_Mail::EMAIL_ADDRESS_REGEXP, $punyCodedAddress)) {
463                         $invalidEmailAddresses[] = $address;
464                         continue;
465                     }
466
467                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::'
468                         . __LINE__ . ' Add ' . $type . ' address: ' . $punyCodedAddress);
469                     
470                     switch($type) {
471                         case 'to':
472                             $_mail->addTo($punyCodedAddress);
473                             $nonPrivateRecipients[] = $punyCodedAddress;
474                             break;
475                         case 'cc':
476                             $_mail->addCc($punyCodedAddress);
477                             $nonPrivateRecipients[] = $punyCodedAddress;
478                             break;
479                         case 'bcc':
480                             $_mail->addBcc($punyCodedAddress);
481                             break;
482                     }
483                 }
484             }
485         }
486
487         if (count($invalidEmailAddresses) > 0) {
488             $translation = Tinebase_Translation::getTranslation('Felamimail');
489             $messageText = '<' . implode(',', $invalidEmailAddresses) . '>: ' . $translation->_('Invalid address format');
490             $fe = new Felamimail_Exception($messageText);
491             throw $fe;
492         }
493         
494         return $nonPrivateRecipients;
495     }
496
497     protected function _getErrorException($messageText)
498     {
499         $translation = Tinebase_Translation::getTranslation('Felamimail');
500         $message = sprintf($translation->_('Error: %s'), $messageText);
501         $tesg = new Tinebase_Exception_SystemGeneric($message);
502         $tesg->setTitle($translation->_('Could not send message'));
503
504         return $tesg;
505     }
506     
507     /**
508      * set headers in mail to be sent
509      * 
510      * @param Tinebase_Mail $_mail
511      * @param Felamimail_Model_Account $_account
512      * @param Felamimail_Model_Message $_message
513      */
514     protected function _setMailHeaders(Zend_Mail $_mail, Felamimail_Model_Account $_account, Felamimail_Model_Message $_message = NULL)
515     {
516         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Setting mail headers');
517         
518         // add user agent
519         $_mail->addHeader('User-Agent', Tinebase_Core::getTineUserAgent('Email Client'));
520         
521         // set organization
522         if (isset($_account->organization) && ! empty($_account->organization)) {
523             $_mail->addHeader('Organization', $_account->organization);
524         }
525
526         // add reply-to
527         if (! empty($_account->reply_to)) {
528             $_mail->setReplyTo($_account->reply_to, $this->_getSenderName($_account));
529         }
530         
531         // set message-id (we could use Zend_Mail::createMessageId() here)
532         if ($_mail->getMessageId() === NULL) {
533             $domainPart = substr($_account->email, strpos($_account->email, '@'));
534             $uid = Tinebase_Record_Abstract::generateUID();
535             $_mail->setMessageId($uid . $domainPart);
536         }
537         
538         if ($_message !== NULL) {
539             if ($_message->flags && $_message->flags == Zend_Mail_Storage::FLAG_ANSWERED && $_message->original_id instanceof Felamimail_Model_Message) {
540                 $this->_addReplyHeaders($_message);
541             }
542             
543             // set the header request response
544             if ($_message->reading_conf) {
545                 $_mail->addHeader('Disposition-Notification-To', $_message->from_email);
546             }
547             
548             // add other headers
549             if (! empty($_message->headers) && is_array($_message->headers)) {
550                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
551                     . ' Adding custom headers: ' . print_r($_message->headers, TRUE));
552                 foreach ($_message->headers as $key => $value) {
553                     $value = $this->_trimHeader($key, $value);
554                     $_mail->addHeader($key, $value);
555                 }
556             }
557         }
558     }
559     
560     /**
561      * trim message headers (Zend_Mail only supports < 998 chars)
562      * 
563      * @param string $value
564      * @return string
565      */
566     protected function _trimHeader($key, $value)
567     {
568         if (strlen($value) + strlen($key) > 998) {
569             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
570                 . ' Trimming header ' . $key);
571             
572             $value = substr(trim($value), 0, (995 - strlen($key)));
573
574             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
575                 . $value);
576         }
577         
578         return $value;
579     }
580     
581     /**
582      * set In-Reply-To and References headers
583      * 
584      * @param Felamimail_Model_Message $message
585      * 
586      * @see http://www.faqs.org/rfcs/rfc2822.html / Section 3.6.4.
587      */
588     protected function _addReplyHeaders(Felamimail_Model_Message $message)
589     {
590         $originalHeaders = Felamimail_Controller_Message::getInstance()->getMessageHeaders($message->original_id);
591         if (! isset($originalHeaders['message-id'])) {
592             // no message-id -> skip this
593             return;
594         }
595
596         $messageHeaders = is_array($message->headers) ? $message->headers : array();
597         $messageHeaders['In-Reply-To'] = $originalHeaders['message-id'];
598         
599         $references = '';
600         if (isset($originalHeaders['references'])) {
601             $references = $originalHeaders['references'] . ' ';
602         } else if (isset($originalHeaders['in-reply-to'])) {
603             $references = $originalHeaders['in-reply-to'] . ' ';
604         }
605         $references .= $originalHeaders['message-id'];
606         $messageHeaders['References'] = $references;
607         
608         $message->headers = $messageHeaders;
609     }
610     
611     /**
612      * add attachments to mail
613      *
614      * @param Tinebase_Mail $_mail
615      * @param Felamimail_Model_Message $_message
616      * @throws Felamimail_Exception_IMAP
617      */
618     protected function _addAttachments(Tinebase_Mail $_mail, Felamimail_Model_Message $_message)
619     {
620         if (! isset($_message->attachments) || empty($_message->attachments)) {
621             return;
622         }
623
624         $maxAttachmentSize = $this->_getMaxAttachmentSize();
625         $totalSize = 0;
626
627         foreach ($_message->attachments as $attachment) {
628             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
629                 . ' Adding attachment: ' . (is_object($attachment) ? print_r($attachment->toArray(), TRUE) : print_r($attachment, TRUE)));
630
631             if (isset($attachment['type'])
632                 && $attachment['type'] == Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822
633                 && $_message->original_id instanceof Felamimail_Model_Message
634             ) {
635                 $part = $this->_getRfc822Attachment($attachment, $_message);
636
637             } else if (isset($attachment['type'])
638                 && $attachment['type'] == 'filenode'
639             ) {
640                 $part = $this->_getFileNodeAttachment($attachment);
641
642             } else if ($attachment instanceof Tinebase_Model_TempFile || isset($attachment['tempFile'])) {
643                 $part = $this->_getTempFileAttachment($attachment);
644
645             } else {
646                 $part = $this->_getMessagePartAttachment($attachment);
647             }
648
649             if (! $part || empty($attachment['type'])) {
650                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
651                     . ' Skipping attachment ' . print_r($attachment, true));
652                 continue;
653             }
654             
655             $part->setTypeAndDispositionForAttachment($attachment['type'], $attachment['name']);
656
657             if (! empty($attachment['size'])) {
658                 $totalSize += $attachment['size'];
659             }
660             
661             if ($totalSize > $maxAttachmentSize) {
662                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
663                     . ' Current attachment size: ' . Tinebase_Helper::convertToMegabytes($totalSize) . ' MB / allowed size: '
664                     . Tinebase_Helper::convertToMegabytes($maxAttachmentSize) . ' MB');
665                 throw new Felamimail_Exception_IMAP('Maximum attachment size exceeded. Please remove one or more attachments.');
666             }
667             
668             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
669                 . ' Adding attachment ' . $part->type);
670             
671             $_mail->addAttachment($part);
672         }
673     }
674
675     /**
676      * get attachment of type CONTENT_TYPE_MESSAGE_RFC822
677      *
678      * @param $attachment
679      * @param $message
680      * @return Zend_Mime_Part
681      */
682     protected function _getRfc822Attachment(&$attachment, $message)
683     {
684         $part = $this->getMessagePart($message->original_id, ($message->original_part_id) ? $message->original_part_id : NULL);
685         $part->decodeContent();
686
687         // replace some chars from attachment name
688         $attachment['name'] = preg_replace("/[\s'\"]*/", "", $attachment['name']) . '.eml';
689
690         return $part;
691     }
692
693     /**
694      * get attachment defined by a file node (mailfiler or filemanager)
695      *
696      * @param $attachment
697      * @return null|Zend_Mime_Part
698      * @throws Tinebase_Exception_NotFound
699      *
700      * TODO support Filemanager files
701      * TODO allow to omit $messageuid, $partId
702      * TODO write a test for this
703      */
704     protected function _getFileNodeAttachment(&$attachment)
705     {
706         list($appname, $path, $messageuid, $partId) = explode('|', $attachment['id']);
707
708         $nodeController = Tinebase_Core::getApplicationInstance($appname . '_Model_Node');
709
710         // remove filename from path
711         // TODO remove DRY with \MailFiler_Frontend_Http::downloadAttachment
712         $pathParts = explode('/', $path);
713         array_pop($pathParts);
714         $path = implode('/', $pathParts);
715
716         $filter = array(
717             array(
718                 'field'    => 'path',
719                 'operator' => 'equals',
720                 'value'    => $path
721             ),
722             array(
723                 'field'    => 'messageuid',
724                 'operator' => 'equals',
725                 'value'    => $messageuid
726             ));
727         $node = $nodeController->search(new MailFiler_Model_NodeFilter($filter))->getFirstRecord();
728         if ($node) {
729             if ($appname === 'MailFiler') {
730                 $mailpart = MailFiler_Controller_Message::getInstance()->getPartFromNode($node, $partId);
731
732                 // TODO use streams
733                 $content = Felamimail_Message::getDecodedContent($mailpart);
734                 $part = new Zend_Mime_Part($content);
735                 $part->encoding = Zend_Mime::ENCODING_BASE64;
736
737             } else {
738                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
739                     . ' We don\'t support ' . $appname . ' nodes as attachment yet.');
740             }
741         } else {
742             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
743                 . ' Could not find file node attachment');
744             $part = null;
745         }
746
747         return $part;
748     }
749
750     /**
751      * get attachment defined by temp file
752      *
753      * @param $attachment
754      * @return null|Zend_Mime_Part
755      * @throws Tinebase_Exception_NotFound
756      */
757     protected function _getTempFileAttachment(&$attachment)
758     {
759         $tempFileBackend = Tinebase_TempFile::getInstance();
760         $tempFile = ($attachment instanceof Tinebase_Model_TempFile)
761             ? $attachment
762             : (((isset($attachment['tempFile']) || array_key_exists('tempFile', $attachment))) ? $tempFileBackend->get($attachment['tempFile']['id']) : NULL);
763
764         if ($tempFile === null) {
765             return null;
766         }
767
768         if (! $tempFile->path) {
769             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Could not find attachment.');
770             return null;
771         }
772
773         // get contents from uploaded file
774         $stream = fopen($tempFile->path, 'r');
775         $part = new Zend_Mime_Part($stream);
776
777         // RFC822 attachments are not encoded, set all others to ENCODING_BASE64
778         $part->encoding = ($tempFile->type == Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822) ? null : Zend_Mime::ENCODING_BASE64;
779
780         $attachment['name'] = $tempFile->name;
781         $attachment['type'] = $tempFile->type;
782
783         if (! empty($tempFile->size)) {
784             $attachment['size'] = $tempFile->size;
785         }
786
787         return $part;
788     }
789
790     /**
791      * get attachment part defined by message id + part id
792      *
793      * @param $attachment
794      * @return null|Zend_Mime_Part
795      */
796     protected function _getMessagePartAttachment(&$attachment)
797     {
798         if (! isset($attachment['id']) || strpos($attachment['id'], '_') === false) {
799             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' No valid message id/part id');
800             return null;
801         }
802
803         // might be an attachment defined by message id + part id -> fetch this and attach
804         list($messageId, $partId) = explode('_', $attachment['id']);
805         $part = $this->getMessagePart($messageId, $partId);
806         $part->decodeContent();
807
808         return $part;
809     }
810     
811     /**
812      * get max attachment size for outgoing mails
813      * 
814      * - currently it is set to memory_limit / 10
815      * - returns size in Bytes
816      * 
817      * @return integer
818      */
819     protected function _getMaxAttachmentSize()
820     {
821         $configuredMemoryLimit = ini_get('memory_limit');
822         
823         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
824             . ' memory_limit = ' . $configuredMemoryLimit);
825         
826         if ($configuredMemoryLimit === FALSE or $configuredMemoryLimit == -1) {
827             // set to a big default value
828             $configuredMemoryLimit = '512M';
829         }
830         
831         return Tinebase_Helper::convertToBytes($configuredMemoryLimit) / 10;
832     }
833 }