diff --git a/bot.php b/bot.php index 4e52137..abba031 100755 --- a/bot.php +++ b/bot.php @@ -1,190 +1,245 @@ #!/usr/bin/php SENDER_BLACKLIST ?? []; + // load the IMAPSpooler class require IMAPBOT_PATH_SPOOLER; // declare used classes use reyboz\IMAPSpooler; // imap connection $spooler = new IMAPSpooler( IMAPBOT_IMAP_MAILBOX, IMAPBOT_IMAP_USERNAME, IMAPBOT_IMAP_PASSWORD ); // import phabricator stuff $phab_root = IMAPBOT_PATH_PHAB; require_once $phab_root.'/scripts/__init_script__.php'; require_once $phab_root.'/externals/mimemailparser/MimeMailParser.class.php'; // set the callback for every e-mail -$spooler->setEmailHandler( function ( $body, $headers, $info ) { +$spooler->setEmailHandler( function ( $body, $headers, $info ) use ( $sender_blacklist ) { # The following code is under Apache license (c) Phabricator, Phacility # https://secure.phabricator.com/source/phabricator/browse/master/scripts/mail/mail_handler.php # https://secure.phabricator.com/source/phabricator/browse/master/LICENSE // complete message $message = $headers . $body; $parser = new MimeMailParser(); $parser->setText( $message ); $content = array(); foreach (array('text', 'html') as $part) { $part_body = $parser->getMessageBody($part); if (strlen($part_body) && !phutil_is_utf8($part_body)) { $part_headers = $parser->getMessageBodyHeaders($part); if (!is_array($part_headers)) { $part_headers = array(); } $content_type = idx($part_headers, 'content-type'); if (preg_match('/charset="(.*?)"/', $content_type, $matches) || preg_match('/charset=(\S+)/', $content_type, $matches)) { $part_body = phutil_utf8_convert($part_body, 'UTF-8', $matches[1]); } } $content[$part] = $part_body; } + // parse the email headers $headers = $parser->getHeaders(); $headers['subject'] = phutil_decode_mime_header($headers['subject']); $headers['from'] = phutil_decode_mime_header($headers['from']); - message( "process {$headers['from']}" ); + // shortcut for the "From:" email address + $from = $headers['from']; + + /* + * Check if the "From:" email address is blacklisted + * + * Actually the comparison is strict because this is not intended to fight spam + * anyway feel free to add a regex or something like that. + */ + if( $sender_blacklist && is_from_blacklisted( $from, $sender_blacklist ) ) { + + // show in the log that this email will be discarded + message( "delete blacklisted $from" ); + + // just mark this message for deletion and skip further processing + return true; + } + + // log the processed email addresses just for fun + message( "process $from" ); $received = new PhabricatorMetaMTAReceivedMail(); $received->setHeaders($headers); $received->setBodies($content); $attachments = array(); foreach ($parser->getAttachments() as $attachment) { if (preg_match('@text/(plain|html)@', $attachment->getContentType()) && - $attachment->getContentDisposition() == 'inline') { + $attachment->getContentDisposition() === 'inline') { // If this is an "inline" attachment with some sort of text content-type, // do not treat it as a file for attachment. MimeMailParser already picked // it up in the getMessageBody() call above. We still want to treat 'inline' // attachments with other content types (e.g., images) as attachments. continue; } $file = PhabricatorFile::newFromFileData( $attachment->getContent(), array( 'name' => $attachment->getFilename(), 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, )); $attachments[] = $file->getPHID(); } $delete = true; try { $received->setAttachments($attachments); $received->save(); $received->processReceivedMail(); } catch (Exception $e) { - // do not delete messages with errors - $delete = false; + // delete messages with errors because Phabricator can reply to the sender + // $delete = false; + // if Phabricator detects an exection, contact the sender with that message $received ->setMessage(pht('EXCEPTION: %s', $e->getMessage())) ->save(); - throw $e; + // log some errors + message( $e->getMessage() ); } return $delete; } ); // flag indicating that an error raised $error = false; // flag indicating that we can continue to scan again the mailbox $loop = true; // let's go -echo date('c') . " started\n"; +message( "started" ); do { try { // open the connection $spooler->open(); // just process all and then quit $spooler->processAll(); // close the connection $spooler->close(); // wait some time sleep( IMAPBOT_CYCLE_SLEEP ); } catch( Exception $e ) { printf( "Phabricator SMTP bot error (%s): %s\n", get_class( $e ), $e->getMessage() ); - $loop = false; + + // we can't just stop looping because sometime the user sends a wrong command and we get an Exception + // $loop = false; + $error = true; } } while( $loop ); /** * Operating system signal handler * * @param $signo int * @param $siginfo mixed */ function sig_handler( $signo, $siginfo ) { // stop looping $GLOBALS['loop'] = false; // eventually close the spooler $GLOBALS['spooler']->close(); // just warn about this signal message( "quit after SIG $signo" ); // quit if( $GLOBALS['error'] ) { exit( 1 ); } else { exit( 0 ); } } /** * Print a message to standard output with a date * * @param string $message */ function message( $message ) { printf( "[%s] %s\n", date( 'c' ), $message ); } + +/** + * Check if a "From:" email address matches the blacklisted ones + * + * @param string $from Something like 'Foo Bar ' + * @param array $blacklist Array of blacklisted email addresses + * @return boolean + */ +function is_from_blacklisted( $from, $blacklist ) { + + // parse the "From:" email addresses + $addresses = mailparse_rfc822_parse_addresses( $from ); + foreach( $addresses as $address ) { + list( $display, $address, $is_group ) = $address; + + // check if the email address exactly matches one in the blacklist + if( in_array( $address, $blacklist, true ) ) { + return true; + } + } + + return false; +} diff --git a/config-example.php b/config-example.php index d77881f..01d2b9a 100644 --- a/config-example.php +++ b/config-example.php @@ -1,21 +1,27 @@ SENDER_BLACKLIST = [ + 'very-spammy-address@example.com', +];