diff --git a/bot.php b/bot.php index 81161c0..c0616a2 100755 --- a/bot.php +++ b/bot.php @@ -1,288 +1,306 @@ #!/usr/bin/php 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 ( $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'] ?? ''; // shortcut for the "To:" email address $to = $headers['to'] ?? ''; + // shortcut for the "CC:" email address + $cc = $headers['cc'] ?? ''; + /* * Check if the "From:" email address is blacklisted * - * Also, it checks if the address is in the "To:" address. + * Also, it checks if the address is in the "To:" and "CC:" addresses. * 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( $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 ) ) { + // check the To: or the CC: + if( $blacklist_receiver ) { - // show in the log that this email will be discarded - message( "delete blacklisted to $to" ); + // check To: + if( is_address_blacklisted( $to, $blacklist_receiver ) ) { - // just mark this message for deletion and skip further processing - return true; + // show in the log that this email will be discarded + message( "delete blacklisted to $to" ); + + // just mark this message for deletion and skip further processing + return true; + } + + // check CC: + if( is_address_blacklisted( $cc, $blacklist_receiver ) ) { + + // show in the log that this email will be discarded + message( "delete blacklisted CC $cc" ); + + // 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 $addresses Something like 'Foo Bar ' * @param array $blacklist Array of blacklisted email addresses * @return boolean */ function is_address_blacklisted( $addresses, $blacklist ) { // parse the "From:" email addresses $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( $email, $blacklist, true ) ) { return true; } } return false; }