/** * Send an email. * * This is a re-implementation of PHP's mail() function using App Engine * mail API. The function relies on mailparse extension to parse emails. * * @param string $to Receiver, or receivers of the mail. * @param string $subject Subject of the email to be sent. * @param string $message Message to be sent. * @param string $additional_headers optional * String to be inserted at the end of the email header. * @param string $additional_parameters optional * Additional flags to be passed to the mail program. This arugment is * added only to match the signature of PHP's mail() function. The value is * always ignored. * @return bool * TRUE if the message is sent successfully, otherwise return FALSE. * * @see http://php.net/mail */ public static function sendMail($to, $subject, $message, $additional_headers = null, $additional_parameters = null) { $raw_mail = "To: {$to}\r\nSubject: {$subject}\r\n"; if ($additional_headers != null) { $raw_mail .= trim($additional_headers); } $raw_mail .= "\r\n\r\n{$message}"; $mime = mailparse_msg_create(); mailparse_msg_parse($mime, $raw_mail); $root_part = mailparse_msg_get_part_data($mime); // Set sender address based on the following order // 1. "From" header in $additional_headers // 2. "sendmail_from" ini setting // 3. Default address "mailer@<app-id>.appspotmail.com $from = ini_get('sendmail_from'); if (isset($root_part['headers']['from'])) { $from = $root_part['headers']['from']; } if ($from === false || $from == "") { $from = sprintf(self::DEFAULT_SENDER_ADDRESS_FORMAT, AppIdentityService::getApplicationId()); syslog(LOG_WARNING, "mail(): Unable to determine sender's email address from the " . "'sendmail_from' directive in php.ini or from the 'From' " . "header. Falling back to the default {$from}."); } $email = new Message(); try { $email->setSender($from); $email->addTo($root_part['headers']['to']); if (isset($root_part['headers']['cc'])) { $email->AddCc($root_part['headers']['cc']); } if (isset($root_part['headers']['bcc'])) { $email->AddBcc($root_part['headers']['bcc']); } $email->setSubject($root_part['headers']['subject']); $parts = mailparse_msg_get_structure($mime); if (count($parts) > 1) { foreach ($parts as $part_id) { $part = mailparse_msg_get_part($mime, $part_id); self::parseMimePart($part, $raw_mail, $email); } } else { if ($root_part['content-type'] == 'text/plain') { $email->setTextBody($message); } else { if ($root_part['content-type'] == 'text/html') { $email->setHtmlBody($message); } } } $email->send(); } catch (\Exception $e) { trigger_error('mail(): ' . $e->getMessage(), E_USER_WARNING); return false; } return true; }
/** * Parse the Message into parts * @return void * @private */ private function parse() { $structure = mailparse_msg_get_structure($this->resource); $this->parts = array(); foreach ($structure as $part_id) { $part = mailparse_msg_get_part($this->resource, $part_id); $this->parts[$part_id] = mailparse_msg_get_part_data($part); } }
/** * Enter description here... * * @param CerberusParserMessage $message * @return integer */ public static function parseMessage(CerberusParserMessage $message, $options = array()) { /* * options: * 'no_autoreply' */ $logger = DevblocksPlatform::getConsoleLog(); $settings = DevblocksPlatform::getPluginSettingsService(); $helpdesk_senders = CerberusApplication::getHelpdeskSenders(); // Pre-parse mail filters $pre_filters = Model_PreParseRule::getMatches($message); if (is_array($pre_filters) && !empty($pre_filters)) { // Load filter action manifests for reuse $ext_action_mfts = DevblocksPlatform::getExtensions('cerberusweb.mail_filter.action', false); // Loop through all matching filters foreach ($pre_filters as $pre_filter) { // Do something with matching filter's actions foreach ($pre_filter->actions as $action_key => $action) { switch ($action_key) { case 'blackhole': return NULL; break; case 'redirect': @($to = $action['to']); CerberusMail::reflect($message, $to); return NULL; break; case 'bounce': @($msg = $action['message']); @($subject = 'Delivery failed: ' . self::fixQuotePrintableString($message->headers['subject'])); // [TODO] Follow the RFC spec on a true bounce if (null != ($fromAddressInst = CerberusParser::getAddressFromHeaders($message->headers))) { CerberusMail::quickSend($fromAddressInst->email, $subject, $msg); } return NULL; break; default: // Plugin pre-parser filter actions if (isset($ext_action_mfts[$action_key])) { if (null != @($ext_action = $ext_action_mfts[$action_key]->createInstance())) { try { /* @var $ext_action Extension_MailFilterAction */ $ext_action->run($pre_filter, $message); } catch (Exception $e) { } } } break; } } } } $headers =& $message->headers; // From if (null == ($fromAddressInst = CerberusParser::getAddressFromHeaders($headers))) { $logger->err("[Parser] 'From' address could not be created."); return NULL; } // To/Cc/Bcc $to = array(); $sTo = @$headers['to']; $bIsNew = true; if (!empty($sTo)) { // [TODO] Do we still need this RFC address parser? $to = CerberusParser::parseRfcAddress($sTo); } // Subject // Fix quote printable subject (quoted blocks can appear anywhere in subject) $sSubject = ""; if (isset($headers['subject']) && !empty($headers['subject'])) { $sSubject = $headers['subject']; if (is_array($sSubject)) { $sSubject = array_shift($sSubject); } } // The subject can still end up empty after QP decode if (empty($sSubject)) { $sSubject = "(no subject)"; } // Date $iDate = @strtotime($headers['date']); // If blank, or in the future, set to the current date if (empty($iDate) || $iDate > time()) { $iDate = time(); } // Is banned? if (1 == $fromAddressInst->is_banned) { $logger->info("[Parser] Ignoring ticket from banned address: " . $fromAddressInst->email); return NULL; } // Overloadable $enumSpamTraining = ''; // Message Id / References / In-Reply-To @($sMessageId = $headers['message-id']); $body_append_text = array(); $body_append_html = array(); // [mdf]Check attached files before creating the ticket because we may need to overwrite the message-id // also store any contents of rfc822 files so we can include them after the body foreach ($message->files as $filename => $file) { /* @var $file ParserFile */ switch ($file->mime_type) { case 'message/rfc822': $full_filename = $file->tmpname; $mail = mailparse_msg_parse_file($full_filename); $struct = mailparse_msg_get_structure($mail); $msginfo = mailparse_msg_get_part_data($mail); $inline_headers = $msginfo['headers']; if (isset($headers['from']) && (strtolower(substr($headers['from'], 0, 11)) == 'postmaster@' || strtolower(substr($headers['from'], 0, 14)) == 'mailer-daemon@')) { $headers['in-reply-to'] = $inline_headers['message-id']; } break; } } // [JAS] [TODO] References header may contain multiple message-ids to find if (null != ($ids = self::findParentMessage($headers))) { $bIsNew = false; $id = $ids['ticket_id']; $msgid = $ids['message_id']; // Is it a worker reply from an external client? If so, proxy if (null != ($worker_address = DAO_AddressToWorker::getByAddress($fromAddressInst->email))) { $logger->info("[Parser] Handling an external worker response from " . $fromAddressInst->email); if (!DAO_Ticket::isTicketRequester($worker_address->address, $id)) { // Watcher Commands [TODO] Document on wiki/etc if (0 != ($matches = preg_match_all("/\\[(.*?)\\]/i", $message->headers['subject'], $commands))) { @($command = strtolower(array_pop($commands[1]))); $logger->info("[Parser] Worker command: " . $command); switch ($command) { case 'close': DAO_Ticket::updateTicket($id, array(DAO_Ticket::IS_CLOSED => CerberusTicketStatus::CLOSED)); break; case 'take': DAO_Ticket::updateTicket($id, array(DAO_Ticket::NEXT_WORKER_ID => $worker_address->worker_id)); break; case 'comment': $comment_id = DAO_TicketComment::create(array(DAO_TicketComment::ADDRESS_ID => $fromAddressInst->id, DAO_TicketComment::CREATED => time(), DAO_TicketComment::TICKET_ID => $id, DAO_TicketComment::COMMENT => $message->body)); return $id; break; default: // Typo? break; } } $attachment_files = array(); $attachment_files['name'] = array(); $attachment_files['type'] = array(); $attachment_files['tmp_name'] = array(); $attachment_files['size'] = array(); $i = 0; foreach ($message->files as $filename => $file) { $attachment_files['name'][$i] = $filename; $attachment_files['type'][$i] = $file->mime_type; $attachment_files['tmp_name'][$i] = $file->tmpname; $attachment_files['size'][$i] = $file->file_size; $i++; } CerberusMail::sendTicketMessage(array('message_id' => $msgid, 'content' => $message->body, 'files' => $attachment_files, 'agent_id' => $worker_address->worker_id)); return $id; } else { // ... worker is a requester, treat as normal $logger->info("[Parser] The external worker was a ticket requester, so we're not treating them as a watcher."); } } else { // Reply: Not sent by a worker /* * [TODO] check that this sender is a requester on the matched ticket * Otherwise blank out the $id */ } } $group_id = 0; if (empty($id)) { // New Ticket $sMask = CerberusApplication::generateTicketMask(); $groups = DAO_Group::getAll(); // Routing new tickets if (null != ($routing_rules = Model_MailToGroupRule::getMatches($fromAddressInst, $message))) { if (is_array($routing_rules)) { foreach ($routing_rules as $rule) { // Only end up with the last 'move' action (ignore the previous) if (isset($rule->actions['move'])) { $group_id = intval($rule->actions['move']['group_id']); // We don't need to move again when running rule actions unset($rule->actions['move']); } } } } // Make sure the group exists if (!isset($groups[$group_id])) { $group_id = null; } // Last ditch effort to check for a default group to deliver to if (empty($group_id)) { if (null != ($default_team = DAO_Group::getDefaultGroup())) { $group_id = $default_team->id; } else { // Bounce return null; } } // [JAS] It's important to not set the group_id on the ticket until the messages exist // or inbox filters will just abort. $fields = array(DAO_Ticket::MASK => $sMask, DAO_Ticket::SUBJECT => $sSubject, DAO_Ticket::IS_CLOSED => 0, DAO_Ticket::FIRST_WROTE_ID => intval($fromAddressInst->id), DAO_Ticket::LAST_WROTE_ID => intval($fromAddressInst->id), DAO_Ticket::CREATED_DATE => $iDate, DAO_Ticket::UPDATED_DATE => $iDate, DAO_Ticket::LAST_ACTION_CODE => CerberusTicketActionCode::TICKET_OPENED); $id = DAO_Ticket::createTicket($fields); // Apply routing actions to our new ticket ID if (isset($routing_rules) && is_array($routing_rules)) { foreach ($routing_rules as $rule) { $rule->run($id); } } } // [JAS]: Add requesters to the ticket if (!empty($fromAddressInst->id) && !empty($id)) { // Don't add a requester if the sender is a helpdesk address if (isset($helpdesk_senders[$fromAddressInst->email])) { $logger->info("[Parser] Not adding ourselves as a requester: " . $fromAddressInst->email); } else { DAO_Ticket::createRequester($fromAddressInst->id, $id); } } // Add the other TO/CC addresses to the ticket // [TODO] This should be cleaned up and optimized if ($settings->get('cerberusweb.core', CerberusSettings::PARSER_AUTO_REQ, 0)) { @($autoreq_exclude_list = $settings->get('cerberusweb.core', CerberusSettings::PARSER_AUTO_REQ_EXCLUDE, '')); $destinations = self::getDestinations($headers); if (is_array($destinations) && !empty($destinations)) { // Filter out any excluded requesters if (!empty($autoreq_exclude_list)) { @($autoreq_exclude = DevblocksPlatform::parseCrlfString($autoreq_exclude_list)); if (is_array($autoreq_exclude) && !empty($autoreq_exclude)) { foreach ($autoreq_exclude as $excl_pattern) { $excl_regexp = DevblocksPlatform::parseStringAsRegExp($excl_pattern); // Check all destinations for this pattern foreach ($destinations as $idx => $dest) { if (@preg_match($excl_regexp, $dest)) { unset($destinations[$idx]); } } } } } foreach ($destinations as $dest) { if (null != ($destInst = CerberusApplication::hashLookupAddress($dest, true))) { // Skip if the destination is one of our senders or the matching TO if (isset($helpdesk_senders[$destInst->email])) { continue; } DAO_Ticket::createRequester($destInst->id, $id); } } } } $attachment_path = APP_STORAGE_PATH . '/attachments/'; // [TODO] This should allow external attachments (S3) $fields = array(DAO_Message::TICKET_ID => $id, DAO_Message::CREATED_DATE => $iDate, DAO_Message::ADDRESS_ID => $fromAddressInst->id); $email_id = DAO_Message::create($fields); // Content DAO_MessageContent::create($email_id, $message->body); // Headers foreach ($headers as $hk => $hv) { DAO_MessageHeader::create($email_id, $hk, $hv); } // [mdf] Loop through files to insert attachment records in the db, and move temporary files if (!empty($email_id)) { foreach ($message->files as $filename => $file) { /* @var $file ParserFile */ //[mdf] skip rfc822 messages since we extracted their content above if ($file->mime_type == 'message/rfc822') { continue; } $fields = array(DAO_Attachment::MESSAGE_ID => $email_id, DAO_Attachment::DISPLAY_NAME => $filename, DAO_Attachment::MIME_TYPE => $file->mime_type, DAO_Attachment::FILE_SIZE => intval($file->file_size)); $file_id = DAO_Attachment::create($fields); if (empty($file_id)) { @unlink($file->tmpname); // remove our temp file continue; } // Make file attachments use buckets so we have a max per directory $attachment_bucket = sprintf("%03d/", mt_rand(1, 100)); $attachment_file = $file_id; if (!file_exists($attachment_path . $attachment_bucket)) { @mkdir($attachment_path . $attachment_bucket, 0770, true); // [TODO] Needs error checking } rename($file->getTempFile(), $attachment_path . $attachment_bucket . $attachment_file); // [TODO] Split off attachments into its own DAO DAO_Attachment::update($file_id, array(DAO_Attachment::FILEPATH => $attachment_bucket . $attachment_file)); } } // Pre-load custom fields if (isset($message->custom_fields) && !empty($message->custom_fields)) { foreach ($message->custom_fields as $cf_id => $cf_val) { if (is_array($cf_val) && !empty($cf_val) || !is_array($cf_val) && 0 != strlen($cf_val)) { DAO_CustomFieldValue::setFieldValue('cerberusweb.fields.source.ticket', $id, $cf_id, $cf_val); } } } // Finalize our new ticket details (post-message creation) if ($bIsNew && !empty($id) && !empty($email_id)) { // First thread (needed for anti-spam) DAO_Ticket::updateTicket($id, array(DAO_Ticket::FIRST_MESSAGE_ID => $email_id)); // Prime the change fields (which a few things like anti-spam might change before we commit) $change_fields = array(DAO_Ticket::TEAM_ID => $group_id); $out = CerberusBayes::calculateTicketSpamProbability($id); if (!empty($group_id)) { @($spam_threshold = DAO_GroupSettings::get($group_id, DAO_GroupSettings::SETTING_SPAM_THRESHOLD, 80)); @($spam_action = DAO_GroupSettings::get($group_id, DAO_GroupSettings::SETTING_SPAM_ACTION, '')); @($spam_action_param = DAO_GroupSettings::get($group_id, DAO_GroupSettings::SETTING_SPAM_ACTION_PARAM, '')); if ($out['probability'] * 100 >= $spam_threshold) { $enumSpamTraining = CerberusTicketSpamTraining::SPAM; switch ($spam_action) { default: case 0: // do nothing break; case 1: // delete $change_fields[DAO_Ticket::IS_CLOSED] = 1; $change_fields[DAO_Ticket::IS_DELETED] = 1; break; case 2: // move $buckets = DAO_Bucket::getAll(); // Verify bucket exists if (!empty($spam_action_param) && isset($buckets[$spam_action_param])) { $change_fields[DAO_Ticket::TEAM_ID] = $group_id; $change_fields[DAO_Ticket::CATEGORY_ID] = $spam_action_param; } break; } } } // end spam training // Save properties if (!empty($change_fields)) { DAO_Ticket::updateTicket($id, $change_fields); } } // Reply notifications (new messages are handled by 'move' listener) if (!$bIsNew) { // Inbound Reply Event $eventMgr = DevblocksPlatform::getEventService(); $eventMgr->trigger(new Model_DevblocksEvent('ticket.reply.inbound', array('ticket_id' => $id))); } // New ticket processing if ($bIsNew) { // Auto reply @($autoreply_enabled = DAO_GroupSettings::get($group_id, DAO_GroupSettings::SETTING_AUTO_REPLY_ENABLED, 0)); @($autoreply = DAO_GroupSettings::get($group_id, DAO_GroupSettings::SETTING_AUTO_REPLY, '')); /* * Send the group's autoreply if one exists, as long as this ticket isn't spam */ if (!isset($options['no_autoreply']) && $autoreply_enabled && !empty($autoreply) && $enumSpamTraining != CerberusTicketSpamTraining::SPAM) { CerberusMail::sendTicketMessage(array('ticket_id' => $id, 'message_id' => $email_id, 'content' => str_replace(array('#ticket_id#', '#mask#', '#subject#', '#timestamp#', '#sender#', '#sender_first#', '#orig_body#'), array($id, $sMask, $sSubject, date('r'), $fromAddressInst->email, $fromAddressInst->first_name, ltrim($message->body)), $autoreply), 'is_autoreply' => true, 'dont_keep_copy' => true)); } } // end bIsNew unset($message); // Re-open and update our date on new replies if (!$bIsNew) { DAO_Ticket::updateTicket($id, array(DAO_Ticket::UPDATED_DATE => time(), DAO_Ticket::IS_WAITING => 0, DAO_Ticket::IS_CLOSED => 0, DAO_Ticket::IS_DELETED => 0, DAO_Ticket::LAST_WROTE_ID => $fromAddressInst->id, DAO_Ticket::LAST_ACTION_CODE => CerberusTicketActionCode::TICKET_CUSTOMER_REPLY)); // [TODO] The TICKET_CUSTOMER_REPLY should be sure of this message address not being a worker } @imap_errors(); // Prevent errors from spilling out into STDOUT return $id; }
/** * Parse the Message into parts * * @return void */ protected function parse() { $structure = mailparse_msg_get_structure($this->resource); $this->parts = []; foreach ($structure as $part_id) { $part = mailparse_msg_get_part($this->resource, $part_id); $this->parts[$part_id] = mailparse_msg_get_part_data($part); } }
$match_array["D-Link Camera Alert: Camera-Living-Room - PIR Sensor Triggered"] = "https://graph.api.smartthings.com/api/smartapps/installations/INSTANCE/%ACTION%?access_token=TOKEN"; $match_array["D-Link Camera Alert: Camera-Elia-Evi - PIR Sensor Triggered"] = "https://graph.api.smartthings.com/api/smartapps/installations/INSTANCE/%ACTION%?access_token=TOKEN"; $match_array["D-Link Camera Alert: Camera-Sam - PIR Sensor Triggered"] = "https://graph.api.smartthings.com/api/smartapps/installations/INSTANCE/%ACTION%?access_token=TOKEN"; $match_array["D-Link Camera Alert: Camera-Loft - PIR Sensor Triggered"] = "https://graph.api.smartthings.com/api/smartapps/installations/INSTANCE/%ACTION%?access_token=TOKEN"; // read email $fd = fopen("php://stdin", "r"); $email = ""; while (!feof($fd)) { $line = fread($fd, 1024); $email .= $line; } fclose($fd); $email_file = "/tmp/email_" . rand(); file_put_contents($email_file, $email); $mime = mailparse_msg_parse_file($email_file); $struct = mailparse_msg_get_structure($mime); foreach ($struct as $st) { $section = mailparse_msg_get_part($mime, $st); $info = mailparse_msg_get_part_data($section); $subject = $info[headers][subject]; $curl_string = $match_array[$subject]; // trigger event active $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, str_replace("%ACTION%", "active", $curl_string)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0); curl_exec($ch); curl_close($ch); sleep(15); // trigger event inactive $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, str_replace("%ACTION%", "inactive", $curl_string));
case 'x-original-to': case 'to': $recipient = iconv_mime_decode($headers[2][$id]); break; case 'from': $sender = iconv_mime_decode($headers[2][$id]); break; case 'subject': $subject = trim(iconv_mime_decode($headers[2][$id])); break; default: } } // Get structure $mp = mailparse_msg_parse_file($tmpFile); $structure = mailparse_msg_get_structure($mp); // Retrieve security code. Doesn't consider invalid e-mail adresses, the e-mail was delivered after all. list($localPart, $domain) = explode("@", $recipient); $mailAuthToken = null; if (strstr($localPart, "+")) { list($prefix, $mailAuthToken, $target) = explode("+", $localPart); } // Get user $userId = 0; if ($mailAuthToken) { $q = $db->buildQuery("select id from users where mail_auth_token='%s'", $mailAuthToken); $userId = $db->getSingleValue($q); } Log::debug("mailAuthToken: {$mailAuthToken}, userId: {$userId}"); $errors = array(); $notices = array();
echo "\nExtract via user function\n"; mailparse_msg_extract_part_file($mime, $fp); echo "\nExtract whole part to output\n"; mailparse_msg_extract_whole_part_file($mime, $fp); echo "\nExtract part from string to output\n"; mailparse_msg_extract_part($mime, $text); fclose($fpdest); fclose($fp); $output = ob_get_contents(); ob_end_clean(); VS($output, "Extract to output\n" . "hello, this is some text hello.\n" . "blah blah blah.\n" . "Extract and return as string\n" . "-->\n" . "hello, this is some text hello.\n" . "blah blah blah.\n" . "\n" . "Extract to open file\n" . "\n" . "rewinding\n" . "hello, this is some text hello.\n" . "blah blah blah.\n" . "\n" . "Extract via user function\n" . "hello, this is some text hello.\n" . "blah blah blah.\n" . "\n" . "Extract whole part to output\n" . "To: fred@bloggs.com\n" . "Mime-Version: 1.0\n" . "Content-Type: text/plain\n" . "Subject: A simple MIME message\n" . "\n" . "hello, this is some text hello.\n" . "blah blah blah.\n" . "\n" . "Extract part from string to output\n" . "hello, this is some text hello.\n" . "blah blah blah.\n"); ////////////////////////////////////////////////////////////////////// $msg = "Received: from mail pickup service by hotmail.com with Microsoft\n" . "SMTPSVC;\n" . "Sat, 18 Feb 2006 22:58:14 -0800\n" . "Received: from 66.178.40.49 by BAY116-DAV8.phx.gbl with DAV;\n" . "Sun, 19 Feb 2006 06:58:13 +0000\n" . "\n" . "test"; $mail = mailparse_msg_create(); mailparse_msg_parse($mail, $msg); $arr = mailparse_msg_get_structure($mail); foreach ($arr as $first => $second) { $section = mailparse_msg_get_part($mail, $second); $info = mailparse_msg_get_part_data($section); $received = array("from mail pickup service by hotmail.com with Microsoft", "from 66.178.40.49 by BAY116-DAV8.phx.gbl with DAV;"); VS($info, array("headers" => array("received" => $received), "starting-pos" => 0, "starting-pos-body" => 200, "ending-pos" => 200, "ending-pos-body" => 200, "line-count" => 6, "body-line-count" => 0, "charset" => "us-ascii", "transfer-encoding" => "8bit", "content-type" => "text/plain", "content-base" => "/")); } ////////////////////////////////////////////////////////////////////// $addresses = array("\":sysmail\"@ Some-Group. Some-Org, Muhammed." . "(I am the greatest) Ali @(the)Vegas.WBA", "\"strange\":\":sysmail\"@ Some-Group. Some-Org, Muhammed." . "(I am the greatest) Ali @(the)Vegas.WBA;"); ob_start(); foreach ($addresses as $first => $second) { $parsed = mailparse_rfc822_parse_addresses($second); foreach ($parsed as $pfirst => $psecond) { $pair = $psecond; echo $pair['display']; echo "\n";
/** * @return array */ public function getStructure() { return mailparse_msg_get_structure($this->resource); }