1<?php 2/** 3 * EGroupware Tracker - Handle incoming mails 4 * 5 * This class handles incoming mails in the async services. 6 * It is an addition for the eGW Tracker app by Ralf Becker 7 * 8 * @link http://www.egroupware.org 9 * @author Oscar van Eijk <oscar.van.eijk-AT-oveas.com> 10 * @package tracker 11 * @copyright (c) 2008 by Oscar van Eijk <oscar.van.eijk-AT-oveas.com> 12 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 13 * @version $Id$ 14 */ 15 16use EGroupware\Api; 17use EGroupware\Api\Link; 18 19use EGroupware\Api\Mail; 20 21class tracker_mailhandler extends tracker_bo 22{ 23 /** 24 * UID of the mailsender, 0 if not recognized 25 * 26 * @var int 27 */ 28 var $mailSender; 29 30 /** 31 * Subject line of the incoming mail 32 * 33 * @var string 34 */ 35 var $mailSubject; 36 37 /** 38 * Text from the mailbody (1st part) 39 * 40 * @var string 41 */ 42 var $mailBody; 43 44 /** 45 * Identification of the mailbox 46 * 47 * @var string 48 */ 49 var $mailBox; 50 51 /** 52 * List with all messages retrieved from the server 53 * 54 * @var array 55 */ 56 var $msgList = array(); 57 58 /** 59 * Mailbox stream 60 * 61 * @var int 62 */ 63 var $mbox; 64 65 /** 66 * smtpObject for autoreplies and worwarding 67 * 68 * @var send object 69 */ 70 var $smtpMail; 71 72 /** 73 * Ticket ID or 0 if not recognize 74 * 75 * @var int 76 */ 77 var $ticketId; 78 79 /** 80 * User ID currently executing. Used in case we execute in fallback 81 * 82 * @var int 83 */ 84 var $originalUser; 85 86 /** 87 * Supported mailservertypes, extracted from parent::mailservertypes 88 * 89 * @var array 90 */ 91 var $serverTypes = array(); 92 93 /** 94 * How much should be logged to the apache error-log 95 * 96 * 0 = Nothing 97 * 1 = only errors 98 * 2 = more debug info 99 * 3 = complete debug info 100 */ 101 const LOG_LEVEL = 0; 102 103 /** 104 * Constructor 105 * @param array mailhandlingConfig - optional mailhandling config, to overrule the config loaded by parent 106 */ 107 function __construct($mailhandlingConfig=null) 108 { 109 parent::__construct(); 110 if (!is_null($mailhandlingConfig)) $this->mailhandling = $mailhandlingConfig; 111 // In case we run in fallback, make sure the original user gets restored 112 $this->originalUser = $this->user; 113 foreach($this->mailservertypes as $typ) 114 { 115 $this->serverTypes[] = $typ[0]; 116 } 117 if (($this->mailBox = self::get_mailbox(0)) === false) 118 { 119 return false; 120 } 121 } 122 123 /** 124 * Destructor, close the stream if not done before. 125 */ 126 function __destruct() 127 { 128 } 129 130 /** 131 * Compare 2 given mailbox settings (for a given set of properties) 132 * @param defaultImap Object $reference reference 133 * @param defaultImap Object $profile compare to reference 134 * @return mixed false/array with the differences found; empty array when no differences for the predefined set of keys are found; false if either one is not of type defaultImap 135 */ 136 static function compareMailboxSettings($reference, $profile) 137 { 138 $diff = array(); 139 if (!($reference instanceof Mail\Imap)) return false; 140 if (!($profile instanceof Mail\Imap)) return false; 141 //error_log(__METHOD__.__LINE__.' Reference:'.get_class($reference)); 142 //error_log(__METHOD__.__LINE__.' Reference:'.array2string($reference)); 143 //error_log(__METHOD__.__LINE__.' Profile:'.get_class($profile)); 144 //error_log(__METHOD__.__LINE__.' Profile:'.array2string($profile)); 145 if (get_class($reference) != get_class($profile)) return false; 146 if ($profile instanceof Mail\Imap) 147 { 148 if ($reference->ImapServerId != $profile->ImapServerId) $diff['ImapServerId']=array('reference'=>$reference->ImapServerId,'profile'=>$profile->ImapServerId); 149 try 150 { 151 if ($reference->acc_imap_host != $profile->acc_imap_host) $diff['acc_imap_host']=array('reference'=>$reference->acc_imap_host,'profile'=>$profile->acc_imap_host); 152 if ($reference->acc_imap_port != $profile->acc_imap_port) $diff['acc_imap_port']=array('reference'=>$reference->acc_imap_port,'profile'=>$profile->acc_imap_port); 153 if ($reference->acc_imap_username != $profile->acc_imap_username) $diff['acc_imap_username']=array('reference'=>$reference->acc_imap_username,'profile'=>$profile->acc_imap_username); 154 if ($reference->acc_imap_password != $profile->acc_imap_password) $diff['acc_imap_password']=array('reference'=>$reference->acc_imap_password,'profile'=>$profile->acc_imap_password); 155 } 156 catch(Exception $e) 157 { 158 unset($e); // not used 159 if ($reference->hostspec != $profile->hostspec) $diff['acc_imap_host']=array('reference'=>$reference->hostspec,'profile'=>$profile->hostspec); 160 if ($reference->port != $profile->port) $diff['acc_imap_port']=array('reference'=>$reference->port,'profile'=>$profile->port); 161 if ($reference->username != $profile->username) $diff['acc_imap_username']=array('reference'=>$reference->username,'profile'=>$profile->username); 162 if ($reference->password != $profile->password) $diff['acc_imap_password']=array('reference'=>$reference->password,'profile'=>$profile->password); 163 } 164 } 165 return $diff; 166 } 167 168 /** 169 * Compose the mailbox identification 170 * 171 * @return string mailbox identification as '{server[:port]/type}folder' 172 */ 173 function get_mailbox($queue = 0) 174 { 175 if (empty($this->mailhandling[$queue]['server'])) 176 { 177 return false; // Or should we default to 'localhost'? 178 } 179 if ($this->mailhandling[$queue]['servertype']<=2) 180 { 181 try 182 { 183 $params['acc_imap_type'] = 'EGroupware\\Api\\Mail\\Imap'; 184 $params['acc_id'] = 'tracker_'.trim($queue); 185 $params['acc_imap_host'] = $this->mailhandling[$queue]['server']; 186 $params['acc_imap_ssl'] = ($this->mailhandling[$queue]['servertype']==2?3:($this->mailhandling[$queue]['servertype']==1?2:0)); 187 $params['acc_imap_port'] = $this->mailhandling[$queue]['serverport']; 188 $params['acc_imap_username'] = $this->mailhandling[$queue]['username']; 189 $params['acc_imap_password'] = $this->mailhandling[$queue]['password']; 190 $params['acc_sieve_enabled'] = false; 191 $params['acc_smtp_type'] = 'EGroupware\\Api\\Mail\\Smtp'; // needed, else the constructor fails 192 $eaaccount = new Mail\Account($params); 193 $icServer = $eaaccount->imapServer(); 194 return $icServer; 195 } 196 catch (Exception $e) 197 { 198 error_log(__METHOD__.__LINE__.' Failed loading mail profile:'.$e->getMessage()); 199 return false; 200 } 201 } 202 } 203 204 /** 205 * Get all mails from the server. Invoked by the async timer 206 * 207 * @param int|string|array $queue Which tracker queue to check mail for (array('tr_tracker' => $queue) 208 * @param boolean TestConnection=false 209 * @return boolean true=run finished, false=an error occured 210 */ 211 function check_mail($queue = 0, $TestConnection=false) 212 { 213 $matches = null; 214 // new format with array('tr_tracker' => $queue) 215 if (is_array($queue)) 216 { 217 $queue = $queue['tr_tracker']; 218 } 219 // remove quotes added by async-service 220 elseif(!is_numeric($queue) && preg_match('/([0-9]+)/', $queue, $matches)) 221 { 222 $queue = $matches[1]; 223 } 224 // Config for all passes null 225 if(!$queue) { 226 $queue = 0; 227 } else { 228 // Mailbox for all is pre-loaded, for others we have to change it 229 $this->mailBox = self::get_mailbox($queue); 230 } 231 if (self::LOG_LEVEL>1) error_log(__METHOD__.__LINE__." for $queue on".' # Instance='.$GLOBALS['egw_info']['user']['domain']); 232 233 if ($this->mailBox === false) 234 { 235 if ($TestConnection) throw new Api\Exception\WrongUserinput(lang("incomplete server profile for mailhandling provided; Disabling mailhandling for Queue %1", $queue)); 236 // this line should prevent adding garbage to mailhandlerconfig 237 if (!isset($this->mailhandling[$queue]) || empty($this->mailhandling[$queue]) || $this->mailhandling[$queue]['interval']==0) return false; 238 error_log(__METHOD__.','.__LINE__.lang("incomplete server profile for mailhandling provided; Disabling mailhandling for Queue %1", $queue.' # Instance='.$GLOBALS['egw_info']['user']['domain'])); 239 $this->mailhandling[$queue]['interval']=0; 240 $this->save_config(); 241 return false; 242 } 243 if ($this->mailBox instanceof Mail\Imap) 244 { 245 if (/*$this->mailhandling[$queue]['auto_reply'] ||*/ $this->mailhandling[$queue]['autoreplies'] || $this->mailhandling[$queue]['unrecognized_mails']) 246 { 247 if(is_object($this->smtpMail)) 248 { 249 unset($this->smtpMail); 250 } 251 try 252 { 253 $this->smtpMail = new Api\Mailer('initbasic'); 254 if (self::LOG_LEVEL>2) error_log(__METHOD__.__LINE__.array2string($this->smtpMail)); 255 } catch(Exception $e) { 256 // ignore exception, but log it, to block the account and give a correct error-message to user 257 error_log(__METHOD__."(".__LINE__.") "." Could not initiate smtpMail for notification purpose:".$e->getMessage()); 258 unset($this->smtpMail); 259 } 260 261 } 262 $rFP=Api\Cache::getCache(Api\Cache::INSTANCE,'email','rememberFailedProfile_'.trim($this->mailBox->ImapServerId)); 263 if ($rFP && !empty($rFP)) 264 { 265 $d = self::compareMailboxSettings($this->mailBox,$rFP); 266 if ($d===false || empty($d)) 267 { 268 if ($TestConnection==false) 269 { 270 error_log(__METHOD__.','.__LINE__." ".lang("eGroupWare Tracker Mailhandling: could not connect previously, and profile did not change")); 271 error_log(__METHOD__.','.__LINE__." ".lang("refused to open mailbox: %1",array2string($this->mailBox))); 272 $previousInterval = $this->mailhandling[$queue]['interval']; 273 $this->mailhandling[$queue]['interval']=$this->mailhandling[$queue]['interval']*2; 274 $this->save_config(); 275 Api\Cache::setCache(Api\Cache::INSTANCE,'email','rememberFailedProfile_'.trim($this->mailBox->ImapServerId),array(),$expiration=60*10); 276 if ($GLOBALS['egw_info']['server']['admin_mails'] && $this->smtpMail) 277 { 278 // notify admin(s) via email 279 $from = 'eGroupWareTrackerMailHandling@'.$GLOBALS['egw_info']['server']['mail_suffix']; 280 $subject = lang("eGroupWare Tracker Mailhandling: could not connect previously, and profile did not change"); 281 $body = lang("refused to open mailbox therefore changed Interval from %1 to %2",$previousInterval,$this->mailhandling[$queue]['interval']); 282 $body .= "\n"; 283 $body .= lang("Mailbox settings used: %1",array2string($this->mailBox)); 284 285 $admin_mails = explode(',',$GLOBALS['egw_info']['server']['admin_mails']); 286 foreach($admin_mails as $to) 287 { 288 try { 289 $GLOBALS['egw']->send->msg('email',$to,$subject,$body,'','','',$from,$from); 290 } 291 catch(Exception $e) { 292 // ignore exception, but log it, to block the account and give a correct error-message to user 293 error_log(__METHOD__."('$to') ".$e->getMessage()); 294 } 295 } 296 } 297 return false; 298 } 299 } 300 } 301 $mailobject = Mail::getInstance(false,$this->mailBox->ImapServerId,false,$this->mailBox); 302 if (self::LOG_LEVEL>2) error_log(__METHOD__.__LINE__.'#'.array2string($this->mailBox)); 303 304 $connectionFailed = false; 305 // connect 306 try 307 { 308 $mailobject->openConnection($this->mailBox->ImapServerId); 309 $_folderName = (!empty($this->mailhandling[$queue]['folder'])?$this->mailhandling[$queue]['folder']:'INBOX'); 310 $mailobject->reopen($_folderName); 311 } 312 catch (Exception $e) 313 { 314 $connectionFailed=true; 315 $mailobjecterrorMessage = $e->getMessage(); 316 } 317 if ($TestConnection===true) 318 { 319 if (self::LOG_LEVEL>0) error_log(__METHOD__.','.__LINE__." failed to open mailbox:".array2string($mailobject->icServer)); 320 if ($connectionFailed) throw new Api\Exception\WrongUserinput(lang("failed to open mailbox: %1 -> disabled for automatic mailprocessing!",($mailobjecterrorMessage?$mailobjecterrorMessage:lang('could not connect')))); 321 return true;//everythig all right 322 } 323 if ($connectionFailed) 324 { 325 Api\Cache::setCache(Api\Cache::INSTANCE,'email','rememberFailedProfile_'.trim($this->mailBox->ImapServerId),$this->mailBox,$expiration=60*60*5); 326 if (self::LOG_LEVEL>0) error_log(__METHOD__.','.__LINE__." failed to open mailbox:".array2string($this->mailBox)); 327 return false; 328 } 329 else 330 { 331 Api\Cache::setCache(Api\Cache::INSTANCE,'email','rememberFailedProfile_'.trim($this->mailBox->ImapServerId),array(),$expiration=60*10); 332 } 333 // load lang stuff for mailheaderInfoSection creation 334 Api\Translation::add_app('mail'); 335 // retrieve list 336 if (self::LOG_LEVEL>1) error_log(__METHOD__.__LINE__." Processing mailbox {$_folderName} with ServerID:".$mailobject->icServer->ImapServerId." for queue $queue\n".array2string($mailobject->icServer)); 337 $_filter=array('status'=>array('UNSEEN','UNDELETED')); 338 if (!empty($this->mailhandling[$queue]['address'])) 339 { 340 $_filter['type']='TO'; 341 $_filter['string']=trim($this->mailhandling[$queue]['address']); 342 } 343 $_reverse=1; 344 $_rByUid = true; 345 $_sortResult = $mailobject->getSortedList($_folderName, $_sort=0, $_reverse, $_filter, $_rByUid, false); 346 $sortResult = $_sortResult['match']->ids; 347 if (self::LOG_LEVEL>1 && $sortResult) error_log(__METHOD__.__LINE__.'#'.array2string($sortResult)); 348 $deletedCounter = 0; 349 $mailobject->reopen($_folderName); 350 foreach ((array)$sortResult as $uid) 351 { 352 if (empty($uid)) continue; 353 if (self::LOG_LEVEL>1) error_log(__METHOD__.__LINE__.'# fetching Data for:'.array2string(array('uid'=>$uid,'folder'=>$_folderName)).' Mode:'.$this->htmledit.' SaveAsOption:'.$GLOBALS['egw_info']['user']['preferences']['mail']['saveAsOptions']); 354 if ($uid) 355 { 356 $this->user = $this->originalUser; 357 $htmlEditOrg = $this->htmledit; // preserve that, as an existing ticket may be of a different mode 358 if (self::process_message2($mailobject, $uid, $_folderName, $queue) && $this->mailhandling[$queue]['delete_from_server']) 359 { 360 try 361 { 362 $mailobject->deleteMessages($uid, $_folderName, 'move_to_trash'); 363 $deletedCounter++; 364 } 365 catch (Exception $e) 366 { 367 error_log(__METHOD__.__LINE__." Failed to move Message (".array2string($uid).") from Folder $_folderName to configured TrashFolder Error:".$e->getMessage()); 368 } 369 } 370 $this->htmledit = $htmlEditOrg; 371 } 372 } 373 // Expunge deleted mails, if any 374 if ($deletedCounter) // NOTE THERE MAY BE DELETED MESSAGES AFTER THE PROCESSING 375 { 376 $mailobject->reopen($_folderName); 377 $rv = $mailobject->compressFolder($_folderName); 378 if (self::LOG_LEVEL && PEAR::isError($rv)) error_log(__METHOD__." failed to expunge Message(s) from Folder: ".$_folderName.' due to:'.$rv->message); 379 } 380 381 // Close the connection 382 //$mailobject->closeConnection(); // not sure we should do that, as this seems to kill more then our connection 383 384 $this->user = $this->originalUser; 385 return true; 386 } 387 } 388 389 /** 390 * determines the mime type of a eMail in accordance to the imap_fetchstructure 391 * found at http://www.linuxscope.net/articles/mailAttachmentsPHP.html 392 * by Kevin Steffer 393 */ 394 function get_mime_type(&$structure) 395 { 396 } 397 398 function get_part($stream, $msg_number, $mime_type, $structure = false, $part_number = false) 399 { 400 } // END OF FUNCTION 401 402 /** 403 * Extract the latest reply from mail body message 404 * 405 * 406 * @param {string} $mailBody string of mail body message 407 * 408 * @return {string} latest reply of mail body message 409 * 410 * @todo Find an optimize and accurate pattern/method to recognize content of mail message (e.g. Recognition/Classification algorithm like Perceptron or similar) 411 */ 412 function extract_latestReply ($mailBody) 413 { 414 $mailCntArray = preg_split("/(\r\n|\n|\r)/",$mailBody); 415 $oMInx = 0; 416 $noReplyMatch = true; 417 foreach (array_keys($mailCntArray) as $key) 418 { 419 if (preg_match("/-----.*".lang("original message")."---.*/i", $mailCntArray[$key]) && $oMInx === 0) 420 { 421 $oMInx = $key; 422 } 423 if (preg_match("/^>.*|\<\/blockquote\>/",$mailCntArray[$key])) 424 { 425 $noReplyMatch = false; 426 if ($oMInx > 0) 427 { 428 for ($i = $oMInx; $i<$key; $i++) 429 { 430 unset ($mailCntArray[$i]); 431 } 432 unset ($mailCntArray[$i]); 433 } 434 } 435 } 436 // try to cleanup original part even if not finding ">" or "blockquote" 437 if ($noReplyMatch && $oMInx > 0) 438 { 439 foreach (array_keys($mailCntArray) as $key) 440 { 441 if ($key >= $oMInx) unset ($mailCntArray[$key]); 442 } 443 } 444 return join("\n", $mailCntArray); 445 } 446 447 /** 448 * Retrieve and decode a bodypart 449 * 450 * @param int Message ID from the server 451 * @param string The body part, defaults to "1" 452 * @return string The decoded bodypart 453 */ 454 function get_mailbody ($mid, $section=false, $structure = false) 455 { 456 } 457 458 /** 459 * Check if this is an automated message (bounce, autoreply...) 460 * @TODO This is currently a very basic implementation, the intention is to implement more checks, 461 * eg, filter failing addresses and remove them from CC. 462 * 463 * @param int $mid Message ID 464 * @param array $msgHeader IMap header 465 * @return boolean 466 */ 467 function is_automail($mid, $msgHeader) 468 { 469 } 470 471 /** 472 * Check if this is an automated message (bounce, autoreply...) 473 * @TODO This is currently a very basic implementation, the intention is to implement more checks, 474 * eg, filter failing addresses and remove them from CC. 475 * 476 * @param object mailobject holding the server, and its connection 477 * @param int message ID from the server 478 * @param string subject the messages subject 479 * @param array msgHeaders full headers retrieved for message 480 * @param int queue the queue we are in 481 * @return boolean status 482 */ 483 function is_automail2($mailobject, $uid, $subject, $msgHeaders, $queue=0) 484 { 485 // This array can be filled with checks that should be made. 486 // 'bounces' and 'autoreplies' (level 1) are the keys coded below, the level 2 arrays 487 // must match $msgHeader properties. 488 // 489 $autoMails = array( 490 'bounces' => array( 491 'subject' => array( 492 ) 493 ,'from' => array( 494 'mailer-daemon' 495 ) 496 ) 497 ,'autoreplies' => array( 498 'subject' => array( 499 'out of the office', 500 'out of office', 501 'autoreply' 502 ) 503 ,'auto-submitted' => array( 504 'auto-replied' 505 ) 506 ) 507 ); 508 509 // Check for bounced messages 510 foreach ($autoMails['bounces'] as $_k => $_v) { 511 if (count($_v) == 0) { 512 continue; 513 } 514 $_re = '/(' . implode('|', $_v) . ')/i'; 515 if (preg_match($_re, $msgHeader[strtoupper($_k)])) { 516 switch ($this->mailhandling[0]['bounces']) { 517 case 'delete' : // Delete, whatever the overall delete setting is 518 $returnVal = $mailobject->deleteMessages($uid, $_folderName, 'move_to_trash'); 519 break; 520 case 'forward' : // Return the status of the forward attempt 521 $returnVal = $this->forward_message2($mailobject, $uid, $mailcontent['subject'], lang("automatic mails (bounces) are configured to be forwarded"), $queue); 522 if ($returnVal) 523 { 524 $mailobject->flagMessages('seen', $uid, $_folderName); 525 $mailobject->flagMessages('forwarded', $uid, $_folderName); 526 } 527 default : // default: 'ignore' 528 break; 529 } 530 return true; 531 } 532 } 533 534 // Check for autoreplies 535 foreach ($autoMails['autoreplies'] as $_k => $_v) { 536 if (count($_v) == 0) { 537 continue; 538 } 539 $_re = '/(' . implode('|', $_v) . ')/i'; 540 if (preg_match($_re, $msgHeader[strtoupper($_k)])) { 541 switch ($this->mailhandling[0]['autoreplies']) { 542 case 'delete' : // Delete, whatever the overall delete setting is 543 $returnVal = $mailobject->deleteMessages($uid, $_folderName, 'move_to_trash'); 544 break; 545 case 'forward' : // Return the status of the forward attempt 546 $returnVal = $this->forward_message2($mailobject, $uid, $mailcontent['subject'], lang("automatic mails (replies) are configured to be forwarded"), $queue); 547 if ($returnVal) 548 { 549 $mailobject->flagMessages('seen', $uid, $_folderName); 550 $mailobject->flagMessages('forwarded', $uid, $_folderName); 551 } 552 break; 553 case 'process' : // Process normally... 554 return false; // ...so act as if it's no automail 555 default : // default: 'ignore' 556 break; 557 } 558 return true; 559 } 560 } 561 } 562 563 /** 564 * Decode a mail header 565 * 566 * @param string Pointer to the (possibly) encoded header that will be changes 567 */ 568 function decode_header (&$header) 569 { 570 } 571 572 /** 573 * Process a messages from the mailbox 574 * 575 * @param int Message ID from the server 576 * @param int queue tracking queue_id 577 * @return boolean true=message successfully processed, false=message couldn't or shouldn't be processed 578 */ 579 function process_message ($mid, $queue) 580 { 581 } 582 583 /** 584 * Process a messages from the mailbox 585 * 586 * @param int mailobject that holds connection to the server 587 * @param int Message ID from the server 588 * @param string _folderName the folder where the messages should reside in 589 * @param int queue tracking queue_id 590 * @return boolean true=message successfully processed, false=message couldn't or shouldn't be processed 591 */ 592 function process_message2 ($mailobject, $uid, $_folderName, $queue) 593 { 594 $senderIdentified = true; 595 $sR = $mailobject->getHeaders($_folderName, $_startMessage=1, 1, 'INTERNALDATE', true, array(), $uid, false); 596 $s = $sR['header'][$uid]; 597 $subject_in = $mailobject->decode_subject($s['subject']);// we use the needed headers for determining beforehand, if we have a new ticket, or a comment 598 // FLAGS - control in case filter wont work 599 $flags = $s;//implicit with retrieved information on getHeaders 600 if ($flags['deleted'] || $flags['seen']) 601 { 602 return false; // Already seen or deleted (in case our filter did not work as intended) 603 } 604 // should do the same as checking only recent, but is more robust as recent is a flag with some sideeffects 605 // message should be marked/flagged as seen after processing 606 // (don't forget to flag the message if forwarded; as forwarded is not supported with all IMAP use Seen instead) 607 if (($flags['recent'] && $flags['seen']) || 608 ($flags['answered'] && $flags['seen']) || // is answered and seen 609 $flags['draft']) // is Draft 610 { 611 if (self::LOG_LEVEL>1) error_log(__METHOD__.__LINE__.':'."UID:$uid in Folder $_folderName with".' Subject:'.$subject_in. 612 "\n Date:".$s['date']. 613 "\n Flags:".print_r($flags,true). 614 "\n Stopped processing Mail ($uid). Not recent, new, or already answered, or draft"); 615 return false; 616 } 617 $subject = Mail::adaptSubjectForImport($subject_in); 618 $tId = $this->get_ticketId($subject); 619 if ($tId) 620 { 621 $t = $this->read($tId); 622 $this->htmledit = $t['tr_edit_mode']=='html'; 623 } 624 $addHeaderInfoSection = false; 625 if (isset($this->mailhandling[$queue]['mailheaderhandling']) && $this->mailhandling[$queue]['mailheaderhandling']>0) 626 { 627 //$tId == 0 will be new ticket, else will indicate comment 628 if ($this->mailhandling[$queue]['mailheaderhandling']==1) $addHeaderInfoSection=($tId == 0 ? true : false); 629 if ($this->mailhandling[$queue]['mailheaderhandling']==2) $addHeaderInfoSection=($tId == 0 ? false: true); 630 if ($this->mailhandling[$queue]['mailheaderhandling']==3) $addHeaderInfoSection=true; 631 } 632 if (self::LOG_LEVEL>1) error_log(__METHOD__.__LINE__."# $uid with title:".$subject.($tId==0?' for new ticket':' for ticket:'.$tId).'. FetchMailHeader:'.$addHeaderInfoSectiont.' mailheaderhandling:'.$this->mailhandling[$queue]['mailheaderhandling']); 633 $mailcontent = $mailobject::get_mailcontent($mailobject,$uid,$partid='',$_folderName,$this->htmledit,$addHeaderInfoSection,(!($GLOBALS['egw_info']['user']['preferences']['mail']['saveAsOptions']==='text_only'))); 634 635 // on we go, as everything seems to be in order. flagging the message 636 $rv = $mailobject->flagMessages('seen', $uid, $_folderName); 637 if ( PEAR::isError($rv)) error_log(__METHOD__.__LINE__." failed to flag Message $uid as Seen in Folder: ".$_folderName.' due to:'.$rv->message); 638 639 // this one adds the mail itself (as message/rfc822 (.eml) file) to the infolog as additional attachment 640 // this is done to have a simple archive functionality 641 if ($mailcontent && $GLOBALS['egw_info']['user']['preferences']['mail']['saveAsOptions']==='add_raw') 642 { 643 $message = $mailobject->getMessageRawBody($uid, $partid, $_folderName); 644 $headers = $mailobject->getMessageHeader($uid, $partid,true,false,$_folderName); 645 $subject = Mail::adaptSubjectForImport($headers['SUBJECT']); 646 $attachment_file =tempnam($GLOBALS['egw_info']['server']['temp_dir'],$GLOBALS['egw_info']['flags']['currentapp']."_"); 647 $tmpfile = fopen($attachment_file,'w'); 648 fwrite($tmpfile,$message); 649 fclose($tmpfile); 650 $size = filesize($attachment_file); 651 $mailcontent['attachments'][] = array( 652 'name' => trim($subject).'.eml', 653 'mimeType' => 'message/rfc822', 654 'type' => 'message/rfc822', 655 'tmp_name' => $attachment_file, 656 'size' => $size, 657 ); 658 } 659 if (self::LOG_LEVEL>1 && $mailcontent) 660 { 661 error_log(__METHOD__.__LINE__.'#'.array2string($mailcontent)); 662 if (!empty($mailcontent['attachments'])) error_log(__METHOD__.__LINE__.'#'.array2string($mailcontent['attachments'])); 663 } 664 if (!$mailcontent) 665 { 666 error_log(__METHOD__.__LINE__." Could not retrieve Content for message $uid in $_folderName for Server with ID:".$mailobject->icServer->ImapServerId." for Queue: $queue"); 667 return false; 668 } 669 // prepare the data to be saved 670 // (use bo function connected to the ui interface mail import, so after preparing we need to adjust stuff) 671 $mailcontent['subject'] = Mail::adaptSubjectForImport($mailcontent['subject']); 672 $this->data = $this->prepare_import_mail( 673 $mailcontent['mailaddress'], 674 $mailcontent['subject'], 675 $mailcontent['message'], 676 $mailcontent['attachments'], 677 ($tId?$tId:0), 678 $queue 679 ); 680 if (self::LOG_LEVEL>2) error_log(__METHOD__.__LINE__.array2string($this->data)); 681 if (self::LOG_LEVEL>2) error_log(__METHOD__.__LINE__.' Mailaddress:'.array2string($mailcontent['mailaddress'])); 682 if (self::LOG_LEVEL>1) error_log(__METHOD__.__LINE__.':'.$this->mailhandling[$queue]['unrecognized_mails'].':'.($this->data['tr_id']?$this->data['reply_creator']:$this->data['tr_creator']).' vs. '.array2string($this->user).' Ticket:'.$this->data['tr_id'].' Message:'.$this->data['msg']); 683 684 // handle auto - mails 685 if ($this->is_automail2($mailobject, $uid, $mailcontent['subject'], $mailcontent['headers'], $queue)) { 686 if (self::LOG_LEVEL>1) error_log(__METHOD__.' Automails will not be processed.'); 687 return false; 688 } 689 if (self::LOG_LEVEL>2) error_log(__METHOD__.__LINE__.array2string($this->data['msg']).':'.$this->data['tr_creator'].'=='.$this->data['reply_creator'].'=='. $this->user); 690 // Handle unrecognized mails: we get a warning from prepare_import_mail, when mail is not recognized 691 // ToDo: Introduce a key, to be able to tell the error-condition 692 if (!empty($this->data['msg']) && (($this->data['tr_creator'] == $this->user) || ($this->data['tr_id'] && $this->data['reply_creator'] == $this->user))) 693 { 694 if (self::LOG_LEVEL>1) error_log(__METHOD__.__LINE__.array2string($this->data['msg']).':'.$this->data['tr_creator'].'=='. $this->user); 695 if ($this->data['tr_id'] && $this->data['reply_creator'] == $this->user) unset($this->data['reply_creator']); 696 $senderIdentified = false; 697 $replytoAddress = $mailcontent['mailaddress']; 698 if (self::LOG_LEVEL>1) error_log(__METHOD__.__LINE__.' ReplyToAddress:'.$replytoAddress); 699 switch ($this->mailhandling[$queue]['unrecognized_mails']) 700 { 701 case 'ignore' : // Do nothing 702 return false; 703 case 'delete' : // Delete, whatever the overall delete setting is 704 $mailobject->deleteMessages($uid, $_folderName, 'move_to_trash'); 705 return false; // Prevent from a second delete attempt 706 case 'forward' : // Return the status of the forward attempt 707 $returnVal = $this->forward_message2($mailobject, $uid, $mailcontent['subject'], $this->data['msg'], $queue); 708 if ($returnVal) 709 { 710 $mailobject->flagMessages('seen', $uid, $_folderName); 711 $mailobject->flagMessages('forwarded', $uid, $_folderName); 712 } 713 return $returnVal; 714 case 'default' : // Save as default user; handled below 715 default : // Duh ?? 716 break; 717 } 718 } 719 else 720 { 721 $replytoAddress = ($this->data['tr_id']?$this->data['reply_creator']:$this->data['tr_creator']); 722 } 723 724 // do not fetch the possible ticketID (again), use what is returned by prepare_import_mail 725 $this->ticketId = $this->data['tr_id']; 726 727 728 if ($this->ticketId == 0) // Create new ticket? 729 { 730 if (empty($this->mailhandling[$queue]['default_tracker'])) 731 { 732 return false; // Not allowed 733 } 734 if (!$senderIdentified) // Unknown user 735 { 736 if (empty($this->mailhandling[$queue]['unrec_mail'])) 737 { 738 return false; // Not allowed for unknown users 739 } 740 $this->mailSender = $this->mailhandling[$queue]['unrec_mail']; // Ok, set default user 741 } 742 } 743 else 744 { 745 $this->mailSender = (!$senderIdentified?$this->mailhandling[$queue]['unrec_mail']:$this->data['reply_creator']); 746 } 747 748 if ($this->ticketId == 0) 749 { 750 751 $this->data['tr_tracker'] = $this->mailhandling[$queue]['default_tracker']; 752 $this->data['cat_id'] = $this->mailhandling[$queue]['default_cat']; 753 $this->data['tr_version'] = $this->mailhandling[$queue]['default_version']; 754 $this->data['tr_priority'] = 5; 755 if (!$senderIdentified && isset($this->mailSender)) $this->data['tr_creator'] = $this->user = $this->mailSender; 756 //error_log(__METHOD__.__LINE__.array2string($this->data)); 757 } 758 else 759 { 760 // Extract latest reply from the mail message content and replace it for last comment 761 $this->data['reply_message'] = $this->extract_latestReply($this->data['reply_message']); 762 763 if (self::LOG_LEVEL>2) error_log(__METHOD__.__LINE__.array2string($this->data['reply_message'])); 764 if (!$senderIdentified) 765 { 766 if (self::LOG_LEVEL>2) error_log(__METHOD__.__LINE__.':'.$this->data['tr_creator'].':'.$this->mailhandling[$queue]['unrec_mail'].':'.$this->user.':'.$this->mailSender.'#'); 767 switch ($this->mailhandling[$queue]['unrec_reply']) 768 { 769 case 0 : 770 $this->user = (!empty($this->data['tr_creator'])?$this->data['tr_creator']:(!empty($this->mailhandling[$queue]['unrec_mail'])?$this->mailhandling[$queue]['unrec_mail']:$this->user)); 771 break; 772 case 1 : 773 $this->user = 0; 774 break; 775 default : 776 $this->user = (!empty($this->mailhandling[$queue]['unrec_mail'])?$this->mailhandling[$queue]['unrec_mail']:0); 777 break; 778 } 779 } 780 else 781 { 782 $this->user = $this->mailSender; 783 } 784 } 785 if ($this->ticketId == 0 && (!isset($this->mailhandling[$queue]['auto_cc']) || $this->mailhandling[$queue]['auto_cc']==false)) 786 { 787 unset($this->data['tr_cc']); 788 } 789 790 if ($this->data['popup']) unset($this->data['popup']); 791 // Save Current edition mode preventing mixed types 792 if ($this->data['tr_edit_mode'] == 'html' && !$this->htmledit) 793 { 794 $this->data['tr_edit_mode'] = 'html'; 795 } 796 elseif ($this->data['tr_edit_mode'] == 'ascii' && $this->htmledit) 797 { 798 $this->data['tr_edit_mode'] = 'ascii'; 799 } 800 else 801 { 802 $this->htmledit ? $this->data['tr_edit_mode'] = 'html' : $this->data['tr_edit_mode'] = 'ascii'; 803 } 804 if (self::LOG_LEVEL>1 && $replytoAddress) error_log(__METHOD__.__LINE__.' Replytoaddress:'.array2string($replytoAddress).' Text:'.$this->mailhandling[$queue]['reply_text']); 805 // Save the ticket and let tracker_bo->save() handle the autorepl, if required 806 $saverv = $this->save(null, 807 (($this->mailhandling[$queue]['auto_reply'] == 2 // Always reply or 808 || ($this->mailhandling[$queue]['auto_reply'] == 1 // only new tickets 809 && $this->ticketId == 0) // and this is a new one 810 ) && ( // AND 811 $senderIdentified // we know this user 812 || (!$senderIdentified // or we don't and 813 && $this->mailhandling[$queue]['reply_unknown'] == 1 // don't care 814 ))) == true 815 ? array( 816 'reply_text' => $this->mailhandling[$queue]['reply_text'], 817 // UserID or mail address 818 'reply_to' => ($replytoAddress ? $replytoAddress : $this->user), 819 ) 820 : null 821 ); 822 // attachments must be saved/linked after saving the ticket 823 if (($saverv==0) && is_array($mailcontent['attachments'])) 824 { 825 foreach ($mailcontent['attachments'] as $attachment) 826 { 827 //error_log(__METHOD__.__LINE__.'#'.$attachment['tmp_name'].'#'.$this->data['tr_id']); 828 if(is_readable($attachment['tmp_name'])) 829 { 830 // Put it where it will be tied to the comment 831 if($this->ticketId) 832 { 833 $attachment['name'] = 'comments/.new/'.$attachment['name']; 834 } 835 836 //error_log(__METHOD__.__LINE__.'# trying to link '.$attachment['tmp_name'].'# to:'.$this->data['tr_id']); 837 Link::attach_file('tracker',$this->data['tr_id'],$attachment); 838 } 839 } 840 $this->comment_files($this->data['tr_id'], 841 $this->data['replies'][0]['reply_id'], 842 $this->data 843 ); 844 } 845 846 return !$saverv; 847 } 848 849 /** 850 * flag message after processing 851 * 852 */ 853 function flagMessageAsSeen($mid, $messageHeader) 854 { 855 } 856 857 /** 858 * Get an email address in plain format, no matter how the address was specified 859 * 860 * @param string $addr a string (probably) containing an email address 861 */ 862 function extract_mailaddress($addr='') 863 { 864 } 865 866 /** 867 * Retrieve the user ID based on the mail address that was extracted from the mailheaders 868 * 869 * @param string $mail_addr ='' the mail address. 870 */ 871 function search_user($mail_addr='') 872 { 873 } 874 875 /** 876 * Forward a mail that was not recognized 877 * 878 * @param int message ID from the server 879 * @return boolean status 880 */ 881 function forward_message($mid=0, &$headers=null, $queue=0) 882 { 883 } 884 885 /** 886 * Forward a mail that was not recognized 887 * 888 * @param object mailobject holding the server, and its connection 889 * @param int message ID from the server 890 * @param string subject the messages subject 891 * @param array _message full retrieved message 892 * @param int queue the queue we are in 893 * @return boolean status 894 */ 895 function forward_message2($mailobject, $uid, $subject, $_message, $queue=0) 896 { 897 $this->smtpMail->ClearAddresses(); 898 $this->smtpMail->clearParts(); 899 $this->smtpMail->AddAddress( 900 $this->mailhandling[$queue]['forward_to'], 901 $this->mailhandling[$queue]['forward_to'] 902 ); 903 904 // Mailer Headers 905 $headers = array ( 906 'X-EGroupware-type' => 'tracker-forward', 907 'X-EGroupware-Tracker' => $queue, 908 'X-EGroupware-Install' => $GLOBALS['egw_info']['server']['install_id']. 909 '@'.$GLOBALS['egw_info']['server']['default_domain'], 910 'Subject' => lang('[FWD]').' '.$subject 911 ); 912 913 $notificationSender = !empty($this->notification[$queue]['sender']) ? 914 $this->notification[$queue]['sender'] : $this->notification[0]['sender']; 915 916 if (!empty($notificationSender)) 917 { 918 $this->smtpMail->setFrom($notificationSender, $notificationSender); 919 } 920 else 921 { 922 $this->smtpMail->setFrom( 923 Api\Accounts::id2name($this->mailSender, 'account_email'), 924 Api\Accounts::id2name($this->mailSender, 'account_lid') 925 ); 926 } 927 928 $this->smtpMail->addHeaders($headers); 929 $this->smtpMail->setBody( 930 lang("This message was forwarded to you from EGroupware-Tracker". 931 " Mailhandling: %1. \r\nSee attachment (original mail)". 932 " for further details\r\n %2", $queue, $_message) 933 ); 934 935 $rawBody = $mailobject->getMessageRawBody( 936 $uid, 937 '', 938 !empty($this->mailhandling[$queue]['folder']) ? $this->mailhandling[$queue]['folder'] : 'INBOX' 939 ); 940 941 $this->smtpMail->AddStringAttachment($rawBody, $subject, 'message/rfc822'); 942 943 if(!$error=$this->smtpMail->Send()) 944 { 945 error_log(__METHOD__.__LINE__." Failed forwarding message via email.$error".print_r($this->smtpMail->ErrorInfo,true)); 946 return false; 947 } 948 if (self::LOG_LEVEL>2) error_log(__METHOD__.__LINE__.array2string($this->smtpMail)); 949 return true; 950 } 951 952 /** 953 * Check if exist and if not start or stop an async job to check incoming mails 954 * 955 * @param int $queue ID of the queue to check email for 956 * @param int $interval =1 >0=start, 0=stop 957 */ 958 static function set_async_job($queue=0, $interval=0) 959 { 960 $async = new Api\Asyncservice(); 961 $job_id = 'tracker-check-mail' . ($queue ? '-'.$queue : ''); 962 963 // Make sure an existing timer is cancelled 964 $async->cancel_timer($job_id); 965 966 if ($interval > 0) 967 { 968 if ($interval == 60) 969 { 970 $async->set_timer(array('hour' => '*'),$job_id,'tracker.tracker_mailhandler.check_mail',array('tr_tracker' => $queue)); 971 } 972 else 973 { 974 $async->set_timer(array('min' => "*/$interval"),$job_id,'tracker.tracker_mailhandler.check_mail',array('tr_tracker' => $queue)); 975 } 976 } 977 } 978} 979