01tonythomas has uploaded a new change for review. https://gerrit.wikimedia.org/r/144656
Change subject: Added API to handle incoming emails automatically ...................................................................... Added API to handle incoming emails automatically Exim should POST the bounce emails via curl using, the pipe config command = /bin/curl curl -d "action=bouncehandler" --data-urlencode "email@-" http://localhost/core/core/api.php This will call the API and pass the argument email to it, so that details can be extracted for processing. Headers are extracted using preg_match. Working verified in local setup. Change-Id: I76fcd68df196170a16646e99095f7517dcb04e40 --- A ApiBounceHandler.php M BounceHandler.php 2 files changed, 180 insertions(+), 1 deletion(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/BounceHandler refs/changes/56/144656/1 diff --git a/ApiBounceHandler.php b/ApiBounceHandler.php new file mode 100644 index 0000000..1d14383 --- /dev/null +++ b/ApiBounceHandler.php @@ -0,0 +1,172 @@ +<?php +/** + * API to handle e-mail bounces + * + */ +class ApiBounceHandler extends ApiBase { + public function execute() { + $email = $this->getMain()->getVal( 'email' ); + $emailHeaders = array(); + $failedUser = array(); + + // Extract headers from raw email + $emailHeaders = self::getHeaders( $email ); + // Extract required header fields + $to = $emailHeaders[ 'to' ]; + $subject = $emailHeaders[ 'subject' ]; + $emailDate = $emailHeaders[ 'date' ]; + + // Get original failed user email and wiki details + $failedUser = self::getUserDetails( $to ); + $wikiId = $failedUser[ 'wikiId' ]; + $originalEmail = $failedUser[ 'rawEmail' ]; + + $bounceTimestamp = wfTimestamp( TS_MW, $emailDate ); + $dbw = wfGetDB( DB_MASTER, array(), $wikiId ); + if( is_array( $failedUser ) ) { + $rowData = array( + 'br_user' => $originalEmail, + 'br_timestamp' => $bounceTimestamp, + 'br_reason' => $subject + ); + $dbw->insert( 'bounce_records', $rowData, __METHOD__ ); + } + self::BounceHandlerActions( $wikiId, $originalEmail, $bounceTimestamp ); + return true; + } + + /** + * Extract the required headers from the received email + * + * @param $email + * @return string + */ + protected function getHeaders( $email ) { + $emailLines = explode( "\n", $email ); + $to = " "; + for ( $i = 0; $i < count( $emailLines ); $i++ ) { + if ( preg_match( "/^To: (.*)/", $emailLines[$i], $toMatch ) ) { + $headers[ 'to' ] = $toMatch[1]; + } + if ( preg_match( "/^Subject: (.*)/", $emailLines[$i], $subjectMatch ) ) { + $headers[ 'subject' ] = $subjectMatch[1]; + } + if ( preg_match( "/^Date: (.*)/", $emailLines[$i], $dateMatch ) ) { + $headers[ 'date' ] = $dateMatch[1]; + } + if ( trim( $emailLines[$i] ) == "" ) { + // Empty line denotes that the header part is finished + break; + } + } + + return $headers; + } + + /** + * Validate and extract user info from a given VERP address and + * + * return the failed user details, if hashes match + * @param string $hashedEmail The original hashed Email from bounce email + * @return array $failedUser The failed user details + * */ + protected function getUserDetails( $hashedEmail ) { + global $wgVERPalgorithm, $wgVERPsecret, $wgVERPAcceptTime; + $currentTime = wfTimestamp(); + $failedUser = array(); + preg_match( '~(.*?)@~', $hashedEmail, $hashedPart ); + $hashedVERPPart = explode( '-', $hashedPart[1] ); + $hashedData = $hashedVERPPart[0]. '-'. $hashedVERPPart[1]. '-'. $hashedVERPPart[2]; + $emailTime = base_convert( $hashedVERPPart[2], 36, 10 ); + if ( hash_hmac( $wgVERPalgorithm, $hashedData, $wgVERPsecret ) === $hashedVERPPart[3] && + $currentTime - $emailTime < $wgVERPAcceptTime ) { + $failedUser[ 'wikiId' ] = str_replace( '.', '-', $hashedVERPPart[0] ); + $rawUserId = base_convert( $hashedVERPPart[1], 36, 10 ); + $failedUser[ 'rawEmail' ] = self::getOriginalEmail( $failedUser, $rawUserId ); + } else { + wfDebugLog( 'BounceHandler', + "Error: Hash validation failed. Expected hash of $hashedData, got $hashedVERPPart[3]." ); + } + + return $failedUser; + } + + /** + * Generate Original Email Id from a hashed emailId + * + * @param array $failedUser The failed user details + * @param string $rawUserId The userId of the failing recipient + * @return string $rawEmail The emailId of the failing recipient + */ + protected function getOriginalEmail( $failedUser, $rawUserId ) { + // In multiple wiki deployed case, the $wikiId can help correctly identify the user after looking up in + // the required database. + $wikiId = $failedUser[ 'wikiId' ]; + $dbr = wfGetDB( DB_SLAVE, array(), $wikiId ); + $res = $dbr->selectRow( + 'user', + array( 'user_email' ), + array( + 'user_id' => $rawUserId, + ), + __METHOD__ + ); + if( $res !== false ) { + $rawEmail = $res->user_email; + } else { + wfDebugLog( 'BounceHandler',"Error fetching email_id of user_id $rawUserId from Database $wikiId." ); + } + + return $rawEmail; + } + + /** + * Perform actions on users who failed to receive emails in a given period + * + * @param string $wikiId The database id of the failing recipient + * @param string $originalEmail The email-id of the failing recipient + * @param string $bounceTimestamp The bounce mail timestamp + * @return bool + */ + protected static function BounceHandlerActions( $wikiId, $originalEmail, $bounceTimestamp ) { + global $wgBounceRecordPeriod, $wgBounceRecordLimit; + $unixTime = wfTimestamp(); + $bounceValidPeriod = wfTimestamp( TS_MW, $unixTime - $wgBounceRecordPeriod ); + $dbr = wfGetDB( DB_SLAVE, array(), $wikiId ); + $res = $dbr->selectRow( 'bounce_records', + array( + 'COUNT(*) as total_count' + ), + array( + 'br_user'=> $originalEmail + ), + __METHOD__ + ); + if( $res !== false ) { + if ( $res->total_count > $wgBounceRecordLimit ) { + //Un-subscribe the user + $dbw = wfGetDB( DB_MASTER, array(), $wikiId ); + $res = $dbw->update( 'user', + array( + 'user_email_authenticated' => null, + 'user_email_token' => null, + 'user_email_token_expires' => null + ), + array( 'user_email' => $originalEmail ), + __METHOD__ + ); + if ( $res ) { + wfDebugLog( 'BounceHandler', "Un-subscribed user $originalEmail for exceeding Bounce + Limit $wgBounceRecordLimit" ); + } else { + wfDebugLog( 'BounceHandler', "Failed to un-subscribe the failing recipient $originalEmail" ); + } + } + } else { + wfDebugLog( 'BounceHandler',"Error fetching the count of past bounces for user $originalEmail" ); + } + + return true; + } + +} \ No newline at end of file diff --git a/BounceHandler.php b/BounceHandler.php index 5e04e45..871fddf 100644 --- a/BounceHandler.php +++ b/BounceHandler.php @@ -21,6 +21,11 @@ //Hooks files $wgAutoloadClasses['BounceHandlerHooks'] = $dir. '/BounceHandlerHooks.php'; +//Register and Load BounceHandler API +$wgAutoloadClasses['ApiBounceHandler'] = $dir. '/ApiBounceHandler.php'; +$wgAPIModules['bouncehandler'] = 'ApiBounceHandler'; + + //Register Hooks $wgHooks['UserMailerChangeReturnPath'][] = 'BounceHandlerHooks::onVERPAddressGenerate'; @@ -46,4 +51,6 @@ /* IMAP configs */ $wgIMAPuser = 'user'; $wgIMAPpass = 'pass'; -$wgIMAPserver = '{localhost:143/imap/novalidate-cert}INBOX'; \ No newline at end of file +$wgIMAPserver = '{localhost:143/imap/novalidate-cert}INBOX'; + +return true; \ No newline at end of file -- To view, visit https://gerrit.wikimedia.org/r/144656 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I76fcd68df196170a16646e99095f7517dcb04e40 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/BounceHandler Gerrit-Branch: master Gerrit-Owner: 01tonythomas <01tonytho...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits