diff --git a/.gitignore b/.gitignore index b74decb..57b2751 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /config.php +/config-* + diff --git a/bot.php b/bot.php index abba031..81161c0 100755 --- a/bot.php +++ b/bot.php @@ -1,245 +1,288 @@ #!/usr/bin/php SENDER_BLACKLIST ?? []; +$blacklist_sender = $IMAPBOT->SENDER_BLACKLIST ?? []; +$blacklist_receiver = $IMAPBOT->RECEIVER_BLACKLIST ?? []; + +// check if we are in debug headers mode +$DEBUG_HEADERS = defined( 'IMAPBOT_DEBUG_HEADERS' ) ? IMAPBOT_DEBUG_HEADERS : false; // 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 ) use ( $sender_blacklist ) { +$spooler->setEmailHandler( function ( $body, $headers, $info ) use ( $blacklist_sender, $blacklist_receiver, $DEBUG_HEADERS ) { # 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']); + // debug mail headers + if( $DEBUG_HEADERS ) { + foreach( $headers as $header => $value ) { + + // simplify message + if( is_array( $value ) ) { + $value = implode( ', ', $value ); + } + + message( sprintf( "HEADER %s: %s", $header, $value ) ); + } + } + // shortcut for the "From:" email address - $from = $headers['from']; + $from = $headers['from'] ?? ''; + + // shortcut for the "To:" email address + $to = $headers['to'] ?? ''; /* * Check if the "From:" email address is blacklisted * + * Also, it checks if the address is in the "To:" address. + * This is useful for mailing lists where the email is sent From a normal user but the + * "To:" is the mailing list itself, while every member receives the email. + * * 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 ) ) { + if( $blacklist_sender && is_address_blacklisted( $from, $blacklist_sender ) ) { + + // show in the log that this email will be discarded + message( "delete blacklisted from $from" ); + + // just mark this message for deletion and skip further processing + return true; + } + + if( $blacklist_receiver && is_address_blacklisted( $to, $blacklist_receiver ) ) { // show in the log that this email will be discarded - message( "delete blacklisted $from" ); + message( "delete blacklisted to $to" ); // 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') { // 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) { // 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(); // 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 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() ); + printf( + " Trace: %s\n", + $e->getTraceAsString() + ); + // 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 string $addresses Something like 'Foo Bar ' * @param array $blacklist Array of blacklisted email addresses * @return boolean */ -function is_from_blacklisted( $from, $blacklist ) { +function is_address_blacklisted( $addresses, $blacklist ) { // parse the "From:" email addresses - $addresses = mailparse_rfc822_parse_addresses( $from ); - foreach( $addresses as $address ) { - list( $display, $address, $is_group ) = $address; + $emails_data = mailparse_rfc822_parse_addresses( $addresses ); + foreach( $emails_data as $email_data ) { + + // address infos + $display = $email_data['display']; + $email = $email_data['address']; + $is_group = $email_data['is_group']; // check if the email address exactly matches one in the blacklist - if( in_array( $address, $blacklist, true ) ) { + if( in_array( $email, $blacklist, true ) ) { return true; } } return false; } diff --git a/config-example.php b/config-example.php index 01d2b9a..9664734 100644 --- a/config-example.php +++ b/config-example.php @@ -1,27 +1,31 @@ SENDER_BLACKLIST = [ - 'very-spammy-address@example.com', +// 'very-spammy-address@example.com', +]; + +$IMAPBOT->RECEIVER_BLACKLIST = [ +// 'very-mailing-list@example.com', ];