0012950: More attachment methods for mail
[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($_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     protected function _getSenderName($account)
438     {
439         return (isset($_account->from) && ! empty($_account->from))
440             ? $_account->from
441             : Tinebase_Core::getUser()->accountFullName;
442     }
443     
444     /**
445      * set mail recipients
446      * 
447      * @param Tinebase_Mail $_mail
448      * @param Felamimail_Model_Message $_message
449      * @return array
450      */
451     protected function _setMailRecipients(Zend_Mail $_mail, Felamimail_Model_Message $_message)
452     {
453         $nonPrivateRecipients = array();
454         $punycodeConverter = $this->getPunycodeConverter();
455         $invalidEmailAddresses = array();
456         
457         foreach (array('to', 'cc', 'bcc') as $type) {
458             if (isset($_message->{$type})) {
459                 foreach((array) $_message->{$type} as $address) {
460
461                     $punyCodedAddress = $punycodeConverter->encode($address);
462
463                     if (! preg_match(Tinebase_Mail::EMAIL_ADDRESS_REGEXP, $punyCodedAddress)) {
464                         $invalidEmailAddresses[] = $address;
465                         continue;
466                     }
467
468                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::'
469                         . __LINE__ . ' Add ' . $type . ' address: ' . $punyCodedAddress);
470                     
471                     switch($type) {
472                         case 'to':
473                             $_mail->addTo($punyCodedAddress);
474                             $nonPrivateRecipients[] = $punyCodedAddress;
475                             break;
476                         case 'cc':
477                             $_mail->addCc($punyCodedAddress);
478                             $nonPrivateRecipients[] = $punyCodedAddress;
479                             break;
480                         case 'bcc':
481                             $_mail->addBcc($punyCodedAddress);
482                             break;
483                     }
484                 }
485             }
486         }
487
488         if (count($invalidEmailAddresses) > 0) {
489             $translation = Tinebase_Translation::getTranslation('Felamimail');
490             $messageText = '<' . implode(',', $invalidEmailAddresses) . '>: ' . $translation->_('Invalid address format');
491             $fe = new Felamimail_Exception($messageText);
492             throw $fe;
493         }
494         
495         return $nonPrivateRecipients;
496     }
497
498     protected function _getErrorException($messageText)
499     {
500         $translation = Tinebase_Translation::getTranslation('Felamimail');
501         $message = sprintf($translation->_('Error: %s'), $messageText);
502         $tesg = new Tinebase_Exception_SystemGeneric($message);
503         $tesg->setTitle($translation->_('Could not send message'));
504
505         return $tesg;
506     }
507     
508     /**
509      * set headers in mail to be sent
510      * 
511      * @param Tinebase_Mail $_mail
512      * @param Felamimail_Model_Account $_account
513      * @param Felamimail_Model_Message $_message
514      */
515     protected function _setMailHeaders(Zend_Mail $_mail, Felamimail_Model_Account $_account, Felamimail_Model_Message $_message = NULL)
516     {
517         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Setting mail headers');
518         
519         // add user agent
520         $_mail->addHeader('User-Agent', Tinebase_Core::getTineUserAgent('Email Client'));
521         
522         // set organization
523         if (isset($_account->organization) && ! empty($_account->organization)) {
524             $_mail->addHeader('Organization', $_account->organization);
525         }
526
527         // add reply-to
528         if (! empty($_account->reply_to)) {
529             $_mail->setReplyTo($_account->reply_to, $this->_getSenderName($_account));
530         }
531         
532         // set message-id (we could use Zend_Mail::createMessageId() here)
533         if ($_mail->getMessageId() === NULL) {
534             $domainPart = substr($_account->email, strpos($_account->email, '@'));
535             $uid = Tinebase_Record_Abstract::generateUID();
536             $_mail->setMessageId($uid . $domainPart);
537         }
538         
539         if ($_message !== NULL) {
540             if ($_message->flags && $_message->flags == Zend_Mail_Storage::FLAG_ANSWERED && $_message->original_id instanceof Felamimail_Model_Message) {
541                 $this->_addReplyHeaders($_message);
542             }
543             
544             // set the header request response
545             if ($_message->reading_conf) {
546                 $_mail->addHeader('Disposition-Notification-To', $_message->from_email);
547             }
548             
549             // add other headers
550             if (! empty($_message->headers) && is_array($_message->headers)) {
551                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
552                     . ' Adding custom headers: ' . print_r($_message->headers, TRUE));
553                 foreach ($_message->headers as $key => $value) {
554                     $value = $this->_trimHeader($key, $value);
555                     $_mail->addHeader($key, $value);
556                 }
557             }
558         }
559     }
560     
561     /**
562      * trim message headers (Zend_Mail only supports < 998 chars)
563      * 
564      * @param string $value
565      * @return string
566      */
567     protected function _trimHeader($key, $value)
568     {
569         if (strlen($value) + strlen($key) > 998) {
570             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
571                 . ' Trimming header ' . $key);
572             
573             $value = substr(trim($value), 0, (995 - strlen($key)));
574
575             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
576                 . $value);
577         }
578         
579         return $value;
580     }
581     
582     /**
583      * set In-Reply-To and References headers
584      * 
585      * @param Felamimail_Model_Message $message
586      * 
587      * @see http://www.faqs.org/rfcs/rfc2822.html / Section 3.6.4.
588      */
589     protected function _addReplyHeaders(Felamimail_Model_Message $message)
590     {
591         $originalHeaders = Felamimail_Controller_Message::getInstance()->getMessageHeaders($message->original_id);
592         if (! isset($originalHeaders['message-id'])) {
593             // no message-id -> skip this
594             return;
595         }
596
597         $messageHeaders = is_array($message->headers) ? $message->headers : array();
598         $messageHeaders['In-Reply-To'] = $originalHeaders['message-id'];
599         
600         $references = '';
601         if (isset($originalHeaders['references'])) {
602             $references = $originalHeaders['references'] . ' ';
603         } else if (isset($originalHeaders['in-reply-to'])) {
604             $references = $originalHeaders['in-reply-to'] . ' ';
605         }
606         $references .= $originalHeaders['message-id'];
607         $messageHeaders['References'] = $references;
608         
609         $message->headers = $messageHeaders;
610     }
611     
612     /**
613      * add attachments to mail
614      *
615      * @param Tinebase_Mail $_mail
616      * @param Felamimail_Model_Message $_message
617      * @throws Felamimail_Exception_IMAP
618      */
619     protected function _addAttachments(Tinebase_Mail $_mail, Felamimail_Model_Message $_message)
620     {
621         if (! isset($_message->attachments) || empty($_message->attachments)) {
622             return;
623         }
624
625         $maxAttachmentSize = $this->_getMaxAttachmentSize();
626         $totalSize = 0;
627
628         foreach ($_message->attachments as $attachment) {
629             $part = $this->_getAttachmentPartByType($attachment, $_message);
630
631             if (! $part || ! isset($attachment['type'])) {
632                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
633                     . ' Skipping attachment ' . print_r($attachment, true));
634                 continue;
635             }
636
637             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
638                 . ' Adding attachment: ' . (is_object($attachment) ? print_r($attachment->toArray(), TRUE) : print_r($attachment, TRUE)));
639
640             $part->setTypeAndDispositionForAttachment($attachment['type'], $attachment['name']);
641
642             if (! empty($attachment['size'])) {
643                 $totalSize += $attachment['size'];
644             }
645             
646             if ($totalSize > $maxAttachmentSize) {
647                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
648                     . ' Current attachment size: ' . Tinebase_Helper::convertToMegabytes($totalSize) . ' MB / allowed size: '
649                     . Tinebase_Helper::convertToMegabytes($maxAttachmentSize) . ' MB');
650                 throw new Felamimail_Exception_IMAP('Maximum attachment size exceeded. Please remove one or more attachments.');
651             }
652             
653             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
654                 . ' Adding attachment ' . $part->type);
655             
656             $_mail->addAttachment($part);
657         }
658     }
659
660     /**
661      * @param $attachment
662      * @return null|Zend_Mime_Part
663      */
664     protected function _getAttachmentPartByType(&$attachment, $_message)
665     {
666         $part = null;
667
668         $attachmentType = $this->_getAttachmentType($attachment, $_message);
669
670         switch ($attachmentType) {
671             case 'rfc822':
672                 $part = $this->_getRfc822Attachment($attachment, $_message);
673                 break;
674             case 'systemlink_fm':
675                 $this->_setSystemlinkAttachment($attachment, $_message);
676                 break;
677             case 'download_public':
678             case 'download_public_fm':
679                 // no attachment part
680                 $this->_setDownloadLinkAttachment($attachment, $_message);
681                 break;
682             case 'download_protected':
683             case 'download_protected_fm':
684                 // no attachment part
685                 $this->_setDownloadLinkAttachment($attachment, $_message, /* protected */ true);
686                 break;
687             case 'filenode':
688                 $part = $this->_getFileNodeAttachment($attachment);
689                 break;
690             case 'tempfile':
691                 $part = $this->_getTempFileAttachment($attachment);
692                 break;
693             default:
694                 $part = $this->_getMessagePartAttachment($attachment);
695         }
696
697         return $part;
698     }
699
700     protected function _getAttachmentType($attachment, $_message)
701     {
702         // Determine if it's a tempfile attachment or a filenode attachment
703         if (isset($attachment['attachment_type']) && $attachment['attachment_type'] === 'attachment' && $attachment['tempFile']) {
704             $attachment['attachment_type'] = 'tempfile';
705         }
706
707         if (isset($attachment['attachment_type']) && $attachment['attachment_type'] === 'attachment' && !$attachment['tempFile']) {
708             $attachment['attachment_type'] = 'filenode';
709         }
710
711         if (isset($attachment['attachment_type'])) {
712             return $attachment['attachment_type'];
713         } elseif (isset($attachment['type'])
714             && $attachment['type'] === Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822
715             && $_message->original_id instanceof Felamimail_Model_Message
716         ) {
717             return 'rfc822';
718         } elseif ($attachment instanceof Tinebase_Model_TempFile || isset($attachment['tempFile'])) {
719             return 'tempfile';
720         }
721
722         return null;
723     }
724
725     /**
726      * get attachment of type CONTENT_TYPE_MESSAGE_RFC822
727      *
728      * @param $attachment
729      * @param $message
730      * @return Zend_Mime_Part
731      */
732     protected function _getRfc822Attachment(&$attachment, $message)
733     {
734         $part = $this->getMessagePart($message->original_id, ($message->original_part_id) ? $message->original_part_id : NULL);
735         $part->decodeContent();
736
737         // replace some chars from attachment name
738         $attachment['name'] = preg_replace("/[\s'\"]*/", "", $attachment['name']) . '.eml';
739
740         return $part;
741     }
742
743     /**
744      * @param            $_attachment
745      * @param            $_message
746      * @param bool|false $_protected
747      * @return boolean success
748      */
749     protected function _setDownloadLinkAttachment($_attachment, $_message, $_protected = false)
750     {
751         if (! Tinebase_Core::getUser()->hasRight('Filemanager', Tinebase_Acl_Rights::RUN)) {
752             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
753                 . ' No right to run Filemanager');
754             return false;
755         }
756
757         $password = $_protected && isset($_attachment['password']) ? $_attachment['password'] : '';
758         $tempFile = $this->_getTempFileFromAttachment($_attachment);
759         if ($tempFile) {
760             $translate = Tinebase_Translation::getTranslation('Felamimail');
761             $downloadLinkFolder = '/' . Tinebase_FileSystem::FOLDER_TYPE_PERSONAL
762                 . '/' . Tinebase_Core::getUser()->getId()
763                 . '/' . $translate->_('.My Mail Download Links');
764             $downloadLink = Filemanager_Controller_Node::getInstance()->createNodeWithDownloadLinkFromTempFile(
765                 $tempFile,
766                 $downloadLinkFolder,
767                 $password
768             );
769         } else {
770             $node = Filemanager_Controller_Node::getInstance()->get($_attachment['id']);
771
772             if (!Tinebase_Core::getUser()->hasGrant($node, Tinebase_Model_Grants::GRANT_PUBLISH)) {
773                 return false;
774             }
775
776             $downloadLink = Filemanager_Controller_DownloadLink::getInstance()->create(new Filemanager_Model_DownloadLink(array(
777                 'node_id'       => $node->getId(),
778                 'expiry_date'   => Tinebase_DateTime::now()->addDay(30)->toString(),
779                 'password'      => $password
780             )));
781         }
782
783         $this->_insertDownloadLinkIntoMailBody($downloadLink->url, $_message);
784
785         return true;
786     }
787
788     /**
789      * @param $_attachment
790      * @param $_message
791      * @return bool
792      */
793     protected function _setSystemlinkAttachment($_attachment, $_message)
794     {
795         if (! Tinebase_Core::getUser()->hasRight('Filemanager', Tinebase_Acl_Rights::RUN)) {
796             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
797                 . ' No right to run Filemanager');
798             return false;
799         }
800
801         $node = Filemanager_Controller_Node::getInstance()->get($_attachment['id']);
802
803         $this->_insertDownloadLinkIntoMailBody(Filemanager_Model_node::getDeepLink($node), $_message);
804
805         return true;
806     }
807
808     /**
809      * @param $_link
810      * @param $_message
811      *
812      * TODO insert above signature
813      */
814     protected function _insertDownloadLinkIntoMailBody($_link, $_message)
815     {
816         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
817             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
818                 . ' Inserting download link into mail body: ' . $_link);
819         }
820
821         if ('text/html' === $_message->content_type) {
822             $_message->body .= sprintf(
823                 '<br />%s<br />',
824                 $_link
825             );
826         } else {
827             $_message->body .= "\n" . $_link . "\n";
828         }
829     }
830
831     /**
832      * get attachment defined by a file node (mailfiler or filemanager)
833      *
834      * @param $attachment
835      * @return null|Zend_Mime_Part
836      * @throws Tinebase_Exception_NotFound
837      *
838      */
839     protected function _getFileNodeAttachment(&$attachment)
840     {
841         if (isset($attachment['path'])) {
842             // allow Filemanager?
843             $appname = 'Filemanager';
844             $path = $attachment['path'];
845         } else {
846             list($appname, $path, $messageuid, $partId) = explode('|', $attachment['id']);
847         }
848
849         try {
850             $nodeController = Tinebase_Core::getApplicationInstance($appname . '_Model_Node');
851         } catch (Tinebase_Exception $te) {
852             Tinebase_Exception::log($te);
853             return null;
854         }
855
856         // remove filename from path
857         // TODO remove DRY with \MailFiler_Frontend_Http::downloadAttachment
858         $pathParts = explode('/', $path);
859         array_pop($pathParts);
860         $path = implode('/', $pathParts);
861
862         if ($appname === 'MailFiler') {
863             $filter = array(
864                 array(
865                     'field' => 'path',
866                     'operator' => 'equals',
867                     'value' => $path
868                 ),
869                 array(
870                     'field' => 'messageuid',
871                     'operator' => 'equals',
872                     'value' => $messageuid
873                 )
874             );
875             $node = $nodeController->search(new MailFiler_Model_NodeFilter($filter))->getFirstRecord();
876         } else {
877             $nodeController = Filemanager_Controller_Node::getInstance();
878             $node = $nodeController->get($attachment['id']);
879
880             if (!Tinebase_Core::getUser()->hasGrant($node, Tinebase_Model_Grants::GRANT_DOWNLOAD)) {
881                 return null;
882             }
883
884             $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath(
885                 Filemanager_Controller_Node::getInstance()->addBasePath($node->path)
886             );
887         }
888
889         if ($node) {
890             if ($appname === 'MailFiler') {
891                 $mailpart = MailFiler_Controller_Message::getInstance()->getPartFromNode($node, $partId);
892                 // TODO use stream
893                 $content = Felamimail_Message::getDecodedContent($mailpart);
894
895             } elseif ($appname === 'Filemanager') {
896                 $content = fopen($pathRecord->streamwrapperpath, 'r');
897
898             } else {
899                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
900                     . ' We don\'t support ' . $appname . ' nodes as attachment yet.');
901             }
902
903             $part = new Zend_Mime_Part($content);
904             $part->encoding = Zend_Mime::ENCODING_BASE64;
905
906         } else {
907             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
908                 . ' Could not find file node attachment');
909             $part = null;
910         }
911
912         return $part;
913     }
914
915     /**
916      * get attachment defined by temp file
917      *
918      * @param $attachment
919      * @return null|Zend_Mime_Part
920      * @throws Tinebase_Exception_NotFound
921      */
922     protected function _getTempFileAttachment(&$attachment)
923     {
924         $tempFile = $this->_getTempFileFromAttachment($attachment);
925         if ($tempFile === null) {
926             return null;
927         }
928
929         if (! $tempFile->path) {
930             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Could not find attachment.');
931             return null;
932         }
933
934         // get contents from uploaded file
935         $stream = fopen($tempFile->path, 'r');
936         $part = new Zend_Mime_Part($stream);
937
938         // RFC822 attachments are not encoded, set all others to ENCODING_BASE64
939         $part->encoding = ($tempFile->type == Felamimail_Model_Message::CONTENT_TYPE_MESSAGE_RFC822) ? null : Zend_Mime::ENCODING_BASE64;
940
941         $attachment['name'] = $tempFile->name;
942         $attachment['type'] = $tempFile->type;
943
944         if (! empty($tempFile->size)) {
945             $attachment['size'] = $tempFile->size;
946         }
947
948         return $part;
949     }
950
951     /**
952      * @param $attachment
953      * @return null|Tinebase_Model_TempFile|Tinebase_Record_Interface
954      * @throws Tinebase_Exception_NotFound
955      */
956     protected function _getTempFileFromAttachment($attachment)
957     {
958         $tempFileBackend = Tinebase_TempFile::getInstance();
959         $tempFile = ($attachment instanceof Tinebase_Model_TempFile)
960             ? $attachment
961             : (((isset($attachment['tempFile']) || array_key_exists('tempFile', $attachment))) ? $tempFileBackend->get($attachment['tempFile']['id']) : NULL);
962
963         return $tempFile;
964     }
965
966     /**
967      * get attachment part defined by message id + part id
968      *
969      * @param $attachment
970      * @return null|Zend_Mime_Part
971      */
972     protected function _getMessagePartAttachment(&$attachment)
973     {
974         if (! isset($attachment['id']) || strpos($attachment['id'], '_') === false) {
975             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' No valid message id/part id');
976             return null;
977         }
978
979         // might be an attachment defined by message id + part id -> fetch this and attach
980         list($messageId, $partId) = explode('_', $attachment['id']);
981         $part = $this->getMessagePart($messageId, $partId);
982         $part->decodeContent();
983
984         return $part;
985     }
986     
987     /**
988      * get max attachment size for outgoing mails
989      * 
990      * - currently it is set to memory_limit / 10
991      * - returns size in Bytes
992      * 
993      * @return integer
994      */
995     protected function _getMaxAttachmentSize()
996     {
997         $configuredMemoryLimit = ini_get('memory_limit');
998         
999         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1000             . ' memory_limit = ' . $configuredMemoryLimit);
1001         
1002         if ($configuredMemoryLimit === FALSE or $configuredMemoryLimit == -1) {
1003             // set to a big default value
1004             $configuredMemoryLimit = '512M';
1005         }
1006         
1007         return Tinebase_Helper::convertToBytes($configuredMemoryLimit) / 10;
1008     }
1009 }