/** * Create and return a Parser class and it's configuration * @param \PhpMimeMailParser\Parser $parsedMail * @param array $arfMail * @return object parser */ public static function create($parsedMail, $arfMail) { /** * Loop through the parser list and try to find a match by * validating the send or the body according to the parsers' * configuration. */ $parsers = Factory::getParsers(); foreach ($parsers as $parserName) { $parserClass = 'AbuseIO\\Parsers\\' . $parserName; // Parser is enabled, see if we can match it's sender_map or body_map if (config("parsers.{$parserName}.parser.enabled") === true) { // Check validity of the 'report_file' setting before we continue // If no report_file is used, continue w/o validation $report_file = config("parsers.{$parserName}.parser.report_file"); if ($report_file == null || is_string($report_file) && isValidRegex($report_file)) { $isValidReport = true; } else { $isValidReport = false; Log::warning('AbuseIO\\Parsers\\Factory: ' . "The parser {$parserName} has an invalid value for 'report_file' (not a regex)."); break; } // Check the sender address foreach (config("parsers.{$parserName}.parser.sender_map") as $senderMap) { if (isValidRegex($senderMap)) { if (preg_match($senderMap, $parsedMail->getHeader('from')) && $isValidReport) { return new $parserClass($parsedMail, $arfMail); } } else { Log::warning('AbuseIO\\Parsers\\Factory: ' . "The parser {$parserName} has an invalid value for 'sender_map' (not a regex)."); } } // If no valid sender is found, check the body foreach (config("parsers.{$parserName}.parser.body_map") as $bodyMap) { if (isValidRegex($bodyMap)) { if (preg_match($bodyMap, $parsedMail->getMessageBody()) && $isValidReport) { return new $parserClass($parsedMail, $arfMail); } if ($arfMail !== false) { foreach ($arfMail as $mailPart) { if (preg_match($bodyMap, $mailPart)) { return new $parserClass($parsedMail, $arfMail); } } } } else { Log::warning('AbuseIO\\Parsers\\Factory: ' . "The parser {$parserName} has an invalid value for 'body_map' (not a regex)."); } } } else { Log::info('AbuseIO\\Parsers\\Factory: ' . "The parser {$parserName} has been disabled and will not be used for this message."); } } // No valid parsers found return false; }
/** * @dataProvider provideData */ public function testFromStream($mid, $subjectExpected, $fromExpected, $toExpected, $textExpected, $htmlExpected, $attachmentsExpected, $countEmbeddedExpected) { //Init $file = __DIR__ . '/mails/' . $mid; $attach_dir = __DIR__ . '/mails/attach_' . $mid . '/'; //Load From Path $Parser = new Parser(); $Parser->setStream(fopen($file, 'r')); //Test Header : subject $this->assertEquals($subjectExpected, $Parser->getHeader('subject')); $this->assertArrayHasKey('subject', $Parser->getHeaders()); //Test Header : from $this->assertEquals($fromExpected, $Parser->getHeader('from')); $this->assertArrayHasKey('from', $Parser->getHeaders()); //Test Header : to $this->assertEquals($toExpected, $Parser->getHeader('to')); $this->assertArrayHasKey('to', $Parser->getHeaders()); //Test Invalid Header $this->assertFalse($Parser->getHeader('azerty')); $this->assertArrayNotHasKey('azerty', $Parser->getHeaders()); //Test Body : text if ($textExpected[0] == 'COUNT') { $this->assertEquals($textExpected[1], substr_count($Parser->getMessageBody('text'), $textExpected[2])); } elseif ($textExpected[0] == 'MATCH') { $this->assertEquals($textExpected[1], $Parser->getMessageBody('text')); } //Test Body : html if ($htmlExpected[0] == 'COUNT') { $this->assertEquals($htmlExpected[1], substr_count($Parser->getMessageBody('html'), $htmlExpected[2])); } elseif ($htmlExpected[0] == 'MATCH') { $this->assertEquals($htmlExpected[1], $Parser->getMessageBody('html')); } //Test Nb Attachments $attachments = $Parser->getAttachments(); $this->assertEquals(count($attachmentsExpected), count($attachments)); $iterAttachments = 0; //Test Attachments $attachmentsEmbeddedToCheck = array(); if (count($attachmentsExpected) > 0) { //Save attachments $Parser->saveAttachments($attach_dir); foreach ($attachmentsExpected as $attachmentExpected) { //Test Exist Attachment $this->assertTrue(file_exists($attach_dir . $attachmentExpected[0])); //Test Filename Attachment $this->assertEquals($attachmentExpected[0], $attachments[$iterAttachments]->getFilename()); //Test Size Attachment $this->assertEquals($attachmentExpected[1], filesize($attach_dir . $attachments[$iterAttachments]->getFilename())); //Test Inside Attachment if ($attachmentExpected[2] != '' && $attachmentExpected[3] > 0) { $fileContent = file_get_contents($attach_dir . $attachments[$iterAttachments]->getFilename(), FILE_USE_INCLUDE_PATH); $this->assertEquals($attachmentExpected[3], substr_count($fileContent, $attachmentExpected[2])); $this->assertEquals($attachmentExpected[3], substr_count($attachments[$iterAttachments]->getContent(), $attachmentExpected[2])); } //Test ContentType Attachment $this->assertEquals($attachmentExpected[4], $attachments[$iterAttachments]->getContentType()); //Test ContentDisposition Attachment $this->assertEquals($attachmentExpected[5], $attachments[$iterAttachments]->getContentDisposition()); //Test md5 of Headers Attachment $this->assertEquals($attachmentExpected[6], md5(serialize($attachments[$iterAttachments]->getHeaders()))); //Save embedded Attachments to check if ($attachmentExpected[7] != '') { array_push($attachmentsEmbeddedToCheck, $attachmentExpected[7]); } //Remove Attachment unlink($attach_dir . $attachments[$iterAttachments]->getFilename()); $iterAttachments++; } //Remove Attachment Directory rmdir($attach_dir); } else { $this->assertFalse($Parser->saveAttachments($attach_dir)); } //Test embedded Attachments $htmlEmbedded = $Parser->getMessageBody('htmlEmbedded'); $this->assertEquals($countEmbeddedExpected, substr_count($htmlEmbedded, "data:")); if (!empty($attachmentsEmbeddedToCheck)) { foreach ($attachmentsEmbeddedToCheck as $itemExpected) { $this->assertEquals(1, substr_count($htmlEmbedded, $itemExpected)); } } }
/** * Return the raw evidence data * TODO: Need to fix json evidence. Needs to be treated the same as eml. * * @return bool|string */ public function getDataAttribute() { $tempFilesystem = Storage::disk('local_temp'); if (Storage::exists($this->filename)) { $data = Storage::get($this->filename); if (is_object(json_decode($data))) { // It's json data return ['headers' => ['from' => $this->sender, 'subject' => $this->subject], 'message' => json_decode($data), 'files' => []]; } else { // It's a regular email, parse it! $cacheDir = $this->getCacheDir(); if (!$tempFilesystem->exists($cacheDir)) { if (!$tempFilesystem->makeDirectory($cacheDir)) { Log::error(get_class($this) . ': ' . 'Unable to create temp directory: ' . $cacheDir); } } $email = new MimeParser(); $email->setText($data); foreach ($email->getAttachments() as $index => $attachment) { $tempFilesystem->put("{$cacheDir}/{$attachment->filename}", $attachment->getContent()); $fileSizes[$index] = strlen($email->getMessageBody('text')); } return ['headers' => ['from' => $email->getHeader('from'), 'subject' => $email->getHeader('subject')], 'message' => $email->getMessageBody('text'), 'files' => $email->getAttachments(), 'files_dir' => $cacheDir]; } } return false; }
/** * Execute the command * * @return void */ public function handle() { Log::info('(JOB ' . getmypid() . ') ' . get_class($this) . ': ' . 'Queued worker is starting the processing of email file: ' . $this->filename); $filesystem = new Filesystem(); $rawEmail = $filesystem->get($this->filename); $parsedMail = new MimeParser(); $parsedMail->setText($rawEmail); // Sanity checks if (empty($parsedMail->getHeader('from')) || empty($parsedMail->getMessageBody())) { Log::warning('(JOB ' . getmypid() . ') ' . get_class($this) . ': ' . 'Missing e-mail headers from and/or empty body: ' . $this->filename); $this->alertAdmin(); return; } // Ignore email from our own notification address to prevent mail loops if (preg_match('/' . Config::get('main.notifications.from_address') . '/', $parsedMail->getHeader('from'))) { Log::warning('(JOB ' . getmypid() . ') ' . get_class($this) . ': ' . 'Loop prevention: Ignoring email from self ' . Config::get('main.notifications.from_address')); $this->alertAdmin(); return; } // Start with detecting valid ARF e-mail $attachments = $parsedMail->getAttachments(); $arfMail = []; foreach ($attachments as $attachment) { if ($attachment->contentType == 'message/feedback-report') { $arfMail['report'] = $attachment->getContent(); } if ($attachment->contentType == 'message/rfc822') { $arfMail['evidence'] = utf8_encode($attachment->getContent()); } if ($attachment->contentType == 'text/plain') { $arfMail['message'] = $attachment->getContent(); } } /* * Sometimes the mime header does not set the main message correctly. This is ment as a fallback and will * use the original content body (which is basicly the same mime element). But only fallback if we actually * have a RFC822 message with a feedback report. */ if (empty($arfMail['message']) && isset($arfMail['report']) && isset($arfMail['evidence'])) { $arfMail['message'] = $parsedMail->getMessageBody(); } // If we do not have a complete e-mail, then we empty the perhaps partially filled arfMail // which is useless, hence reset to false if (!isset($arfMail['report']) || !isset($arfMail['evidence']) || !isset($arfMail['message'])) { $arfMail = false; } // Asking ParserFactory for an object based on mappings, or die trying $parser = ParserFactory::create($parsedMail, $arfMail); if ($parser !== false) { $parserResult = $parser->parse(); } else { Log::error('(JOB ' . getmypid() . ') ' . get_class($this) . ': ' . ': No parser available to handle message from : ' . $parsedMail->getHeader('from') . ' with subject: ' . $parsedMail->getHeader('subject')); $this->alertAdmin(); return; } if ($parserResult !== false && $parserResult['errorStatus'] === true) { Log::error('(JOB ' . getmypid() . ') ' . get_class($parser) . ': ' . ': Parser has ended with fatal errors ! : ' . $parserResult['errorMessage']); $this->alertAdmin(); return; } else { Log::info('(JOB ' . getmypid() . ') ' . get_class($parser) . ': ' . ': Parser completed with ' . $parserResult['warningCount'] . ' warnings and collected ' . count($parserResult['data']) . ' events to save'); } if ($parserResult['warningCount'] !== 0 && Config::get('main.emailparser.notify_on_warnings') === true) { Log::error('(JOB ' . getmypid() . ') ' . get_class($this) . ': ' . 'Configuration has warnings set as critical and ' . $parserResult['warningCount'] . ' warnings were detected. Sending alert to administrator'); $this->alertAdmin(); return; } if (count($parserResult['data']) !== 0) { // Call validator $validator = new EventsValidate(); $validatorResult = $validator->check($parserResult['data']); if ($validatorResult['errorStatus'] === true) { Log::error('(JOB ' . getmypid() . ') ' . get_class($validator) . ': ' . 'Validator has ended with errors ! : ' . $validatorResult['errorMessage']); $this->alertAdmin(); return; } else { Log::info('(JOB ' . getmypid() . ') ' . get_class($validator) . ': ' . 'Validator has ended without errors'); } /** * save evidence into table **/ $evidence = new Evidence(); $evidence->filename = $this->filename; $evidence->sender = $parsedMail->getHeader('from'); $evidence->subject = $parsedMail->getHeader('subject'); $evidence->save(); /** * call saver **/ $saver = new EventsSave(); $saverResult = $saver->save($parserResult['data'], $evidence->id); /** * We've hit a snag, so we are gracefully killing ourselves * after we contact the admin about it. EventsSave should never * end with problems unless the mysql died while doing transactions **/ if ($saverResult['errorStatus'] === true) { Log::error('(JOB ' . getmypid() . ') ' . get_class($saver) . ': ' . 'Saver has ended with errors ! : ' . $saverResult['errorMessage']); $this->alertAdmin(); return; } else { Log::info('(JOB ' . getmypid() . ') ' . get_class($saver) . ': ' . 'Saver has ended without errors'); } } else { Log::warning('(JOB ' . getmypid() . ') ' . get_class($this) . ': ' . 'Parser did not return any events therefore skipping validation and saving a empty event set'); } Log::info('(JOB ' . getmypid() . ') ' . get_class($this) . ': ' . 'Queued worker has ended the processing of email file: ' . $this->filename); }
/** * Execute the command. * * @return void */ public function handle() { Log::info(get_class($this) . ': Queued worker is starting the processing of email file: ' . $this->filename); $filesystem = new Filesystem(); $rawEmail = $filesystem->get($this->filename); $parsedMail = new MimeParser(); $parsedMail->setText($rawEmail); // Sanity checks if (empty($parsedMail->getHeader('from')) || empty($parsedMail->getMessageBody())) { Log::warning(get_class($this) . 'Validation failed on: ' . $this->filename); $this->exception(); } // Ignore email from our own notification address to prevent mail loops if (preg_match('/' . Config::get('main.notifications.from_address') . '/', $parsedMail->getHeader('from'))) { Log::warning(get_class($this) . 'Loop prevention: Ignoring email from self ' . Config::get('main.notifications.from_address')); $this->exception(); } // Start with detecting valid ARF e-mail $attachments = $parsedMail->getAttachments(); $arfMail = []; foreach ($attachments as $attachment) { if ($attachment->contentType == 'message/feedback-report') { $arfMail['report'] = $attachment->getContent(); } if ($attachment->contentType == 'message/rfc822') { $arfMail['evidence'] = $attachment->getContent(); } if ($attachment->contentType == 'text/plain') { $arfMail['message'] = $attachment->getContent(); } } // If we do not have a complete e-mail, then we empty the perhaps partially filled arfMail // which is useless, hence reset to false if (!isset($arfMail['report']) || !isset($arfMail['evidence']) || !isset($arfMail['message'])) { $arfMail = false; } // Asking GetParser for an object based on mappings, or die trying $parser = GetParser::object($parsedMail, $arfMail); $result = false; $events = false; if ($parser !== false) { $result = $parser->parse(); } else { Log::error(get_class($this) . ': Unable to handle message from: ' . $parsedMail->getHeader('from') . ' with subject: ' . $parsedMail->getHeader('subject')); $this->exception(); } if ($result !== false && $result['errorStatus'] !== true) { Log::info(get_class($parser) . ': Parser as ended without errors. Collected ' . count($result['data']) . ' events to save'); $events = $result['data']; } else { Log::error(get_class($parser) . ': Parser as ended with errors ! : ' . $result['errorMessage']); $this->exception(); } // call validater $validator = new EventsValidate($events); $return = $validator->handle(); if ($return['errorStatus'] === false) { Log::error(get_class($validator) . ': Validator as ended with errors ! : ' . $result['errorMessage']); $this->exception(); } else { Log::info(get_class($validator) . ': Validator as ended without errors'); } // save evidence into table $evidence = new Evidence(); $evidence->filename = $this->filename; $evidence->sender = $parsedMail->getHeader('from'); $evidence->subject = $parsedMail->getHeader('subject'); $evidence->save(); // call saver $saver = new EventsSave($events, $evidence->id); $return = $saver->handle(); if ($return['errorStatus'] === false) { Log::error(get_class($saver) . ': Saver as ended with errors ! : ' . $result['errorMessage']); $this->exception(); } else { Log::info(get_class($saver) . ': Saver as ended without errors'); } }
/** * Execute the command. * * @return void */ public function handle() { Log::info(get_class($this) . ': ' . 'Queued worker is starting the processing of email file: ' . $this->filename); $rawEmail = Storage::get($this->filename); $parsedMail = new MimeParser(); $parsedMail->setText($rawEmail); // Sanity checks if (empty($parsedMail->getHeader('from')) || empty($parsedMail->getMessageBody())) { Log::warning(get_class($this) . ': ' . 'Missing e-mail headers from and/or empty body: ' . $this->filename); $this->exception(); return; } // Ignore email from our own notification address to prevent mail loops if (preg_match('/' . Config::get('main.notifications.from_address') . '/', $parsedMail->getHeader('from'))) { Log::warning(get_class($this) . ': ' . 'Loop prevention: Ignoring email from self ' . Config::get('main.notifications.from_address')); $this->exception(); return; } // Start with detecting valid ARF e-mail $attachments = $parsedMail->getAttachments(); $arfMail = []; foreach ($attachments as $attachment) { if ($attachment->contentType == 'message/feedback-report') { $arfMail['report'] = $attachment->getContent(); } if ($attachment->contentType == 'message/rfc822') { $arfMail['evidence'] = utf8_encode($attachment->getContent()); } if ($attachment->contentType == 'text/plain') { $arfMail['message'] = $attachment->getContent(); } } /* * Sometimes the mime header does not set the main message correctly. This is ment as a fallback and will * use the original content body (which is basicly the same mime element). But only fallback if we actually * have a RFC822 message with a feedback report. */ if (empty($arfMail['message']) && isset($arfMail['report']) && isset($arfMail['evidence'])) { $arfMail['message'] = $parsedMail->getMessageBody(); } // If we do not have a complete e-mail, then we empty the perhaps partially filled arfMail // which is useless, hence reset to false if (!isset($arfMail['report']) || !isset($arfMail['evidence']) || !isset($arfMail['message'])) { $arfMail = false; } // Asking ParserFactory for an object based on mappings, or die trying $parser = ParserFactory::create($parsedMail, $arfMail); if ($parser !== false) { $parserResult = $parser->parse(); } else { Log::error(get_class($this) . ': ' . ': No parser available to handle message from : ' . $parsedMail->getHeader('from') . ' with subject: ' . $parsedMail->getHeader('subject')); $this->exception(); return; } if ($parserResult !== false && $parserResult['errorStatus'] === true) { Log::error(get_class($parser) . ': ' . ': Parser has ended with fatal errors ! : ' . $parserResult['errorMessage']); $this->exception(); return; } else { Log::info(get_class($parser) . ': ' . ': Parser completed with ' . $parserResult['warningCount'] . ' warnings and collected ' . count($parserResult['data']) . ' incidents to save'); } if ($parserResult['warningCount'] !== 0 && Config::get('main.emailparser.notify_on_warnings') === true) { Log::error(get_class($this) . ': ' . 'Configuration has warnings set as critical and ' . $parserResult['warningCount'] . ' warnings were detected. Sending alert to administrator'); $this->exception(); return; } /* * build evidence model, but wait with saving it **/ $evidence = new Evidence(); $evidence->filename = $this->filename; $evidence->sender = $parsedMail->getHeader('from'); $evidence->subject = $parsedMail->getHeader('subject'); /* * Call IncidentsProcess to validate, store evidence and save incidents */ $incidentsProcess = new IncidentsProcess($parserResult['data'], $evidence); // Only continue if not empty, empty set is acceptable (exit OK) if (!$incidentsProcess->notEmpty()) { return; } // Validate the data set if (!$incidentsProcess->validate()) { $this->exception(); return; } // Write the data set to database if (!$incidentsProcess->save()) { $this->exception(); return; } Log::info(get_class($this) . ': ' . 'Queued worker has ended the processing of email file: ' . $this->filename); }