Your IP : 216.73.216.54


Current Path : /var/www/html/mediawiki/extensions/BounceHandler/includes/
Upload File :
Current File : /var/www/html/mediawiki/extensions/BounceHandler/includes/ProcessBounceEmails.php

<?php

namespace MediaWiki\Extension\BounceHandler;

use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IDatabase;

/**
 * Class ProcessBounceEmails
 *
 * Methods to process a bounce email
 *
 * @file
 * @ingroup Extensions
 * @author Tony Thomas, Kunal Mehta, Jeff Green
 * @license GPL-2.0-or-later
 */
abstract class ProcessBounceEmails {
	/**
	 * Receive an email from the job queue and process it
	 *
	 * @param string $email
	 */
	abstract public function handleBounce( $email );

	/**
	 * Generates bounce email processor
	 *
	 * @return ProcessBounceWithRegex
	 */
	public static function getProcessor() {
		return new ProcessBounceWithRegex();
	}

	/**
	 * Process bounce email
	 *
	 * @param array $emailHeaders
	 * @param string $emailRaw
	 *
	 * @return bool
	 */
	public function processEmail( $emailHeaders, $emailRaw ) {
		// The bounceHandler needs to respond only to permanent failures.
		$isPermanentFailure = $this->checkPermanentFailure( $emailHeaders );
		if ( $isPermanentFailure ) {
			return $this->processBounceHeaders( $emailHeaders, $emailRaw );
		}

		return false;
	}

	/**
	 * Process received bounce emails from Job Queue
	 *
	 * @param array $emailHeaders
	 * @param string $emailRaw
	 *
	 * @return bool
	 */
	public function processBounceHeaders( $emailHeaders, $emailRaw ) {
		global $wgBounceRecordPeriod, $wgBounceRecordLimit,
			$wgBounceHandlerUnconfirmUsers, $wgBounceRecordMaxAge;

		$to = $emailHeaders['to'];
		$subject = $emailHeaders['subject'];

		// Get original failed user email and wiki details
		$failedUser = $to ? $this->getUserDetails( $to ) : false;
		if ( is_array( $failedUser ) && isset( $failedUser['wikiId'] )
			&& isset( $failedUser['rawEmail'] ) && isset( $failedUser[ 'bounceTime' ] )
		) {
			$wikiId = $failedUser['wikiId'];
			$originalEmail = $failedUser['rawEmail'];
			$bounceTimestamp = $failedUser['bounceTime'];
			$dbw = self::getBounceRecordDB( DB_PRIMARY, $wikiId );

			$rowData = [
				'br_user_email' => $originalEmail,
				'br_timestamp' => $dbw->timestamp( $bounceTimestamp ),
				'br_reason' => $subject
			];
			$dbw->newInsertQueryBuilder()
				->insertInto( 'bounce_records' )
				->row( $rowData )
				->caller( __METHOD__ )
				->execute();
			\MediaWiki\MediaWikiServices::getInstance()->getStatsFactory()
				->withComponent( 'BounceHandler' )
				->getCounter( 'bounces_total' )
				->copyToStatsdAt( 'bouncehandler.bounces' )
				->increment();

			if ( $wgBounceRecordMaxAge ) {
				$pruneOldRecords = new PruneOldBounceRecords( $wgBounceRecordMaxAge );
				$pruneOldRecords->pruneOldRecords( $wikiId );
			}

			$takeBounceActions = new BounceHandlerActions(
				$wikiId,
				$wgBounceRecordPeriod,
				$wgBounceRecordLimit,
				$wgBounceHandlerUnconfirmUsers,
				$emailRaw
			);
			$takeBounceActions->handleFailingRecipient( $failedUser, $emailHeaders );
			return true;
		} else {
			wfDebugLog( 'BounceHandler',
				"Error: Failed to extract user details from verp address $to"
			);
			return false;
		}
	}

	/**
	 * 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
	 */
	public function getUserDetails( $hashedEmail ) {
		global $wgVERPalgorithm, $wgVERPsecret, $wgVERPAcceptTime;

		$failedUser = [];

		$currentTime = (int)wfTimestamp();
		preg_match( '~(.*?)@~', $hashedEmail, $hashedPart );
		if ( !isset( $hashedPart[1] ) ) {
			wfDebugLog( 'BounceHandler',
				"Error: The received address: $hashedEmail does not match the VERP pattern."
			);
			return [];
		}
		$hashedVERPPart = explode( '-', $hashedPart[1] );
		// This would ensure that indexes 0 - 4 in $hashedVERPPart is set
		if ( isset( $hashedVERPPart[4] ) ) {
			$hashedData = $hashedVERPPart[0] . '-' . $hashedVERPPart[1] .
				'-' . $hashedVERPPart[2] . '-' . $hashedVERPPart[3];
		} else {
			wfDebugLog(
				'BounceHandler',
				"Error: Received malformed VERP address: $hashedPart[1], cannot extract details."
			);
			return [];
		}
		$bounceTime = (int)base_convert( $hashedVERPPart[3], 36, 10 );
		// Check if the VERP hash is valid
		if ( base64_encode(
				substr( hash_hmac( $wgVERPalgorithm, $hashedData, $wgVERPsecret, true ), 0, 12 )
			) === $hashedVERPPart[4]
			&& $currentTime - $bounceTime < $wgVERPAcceptTime
		) {
			$failedUser['wikiId'] = str_replace( '.', '-', $hashedVERPPart[1] );
			$failedUser['rawUserId'] = base_convert( $hashedVERPPart[2], 36, 10 );
			$failedEmail = $this->getOriginalEmail( $failedUser );
			$failedUser['rawEmail'] = $failedEmail ? : null;
			$failedUser['bounceTime'] = wfTimestamp( TS_MW, $bounceTime );
		} else {
			wfDebugLog( 'BounceHandler',
				"Error: Hash validation failed. Expected hash of $hashedData, got $hashedVERPPart[4]."
			);
		}

		return $failedUser;
	}

	/**
	 * Generate Original Email Id from a hashed emailId
	 *
	 * @param array $failedUser The failed user details
	 * @return string|false $rawEmail The emailId of the failing recipient
	 */
	public function getOriginalEmail( $failedUser ) {
		// In multiple wiki deployed case, the $wikiId can help correctly
		// identify the user after looking up in the required database.
		$wikiId = $failedUser['wikiId'];
		$rawUserId = $failedUser['rawUserId'];
		$lb = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getMainLB( $wikiId );
		$dbr = $lb->getConnection( DB_REPLICA, [], $wikiId );

		$res = $dbr->newSelectQueryBuilder()
			->select( [ 'user_email' ] )
			->from( 'user' )
			->where( [ 'user_id' => $rawUserId, ] )
			->caller( __METHOD__ )->fetchRow();

		if ( $res !== false ) {
			return $res->user_email;
		}

		wfDebugLog( 'BounceHandler',
			"Error fetching email_id of user_id $rawUserId from Database $wikiId."
		);
		return false;
	}

	/**
	 * Check for a permanent failure
	 *
	 * @param array $emailHeaders
	 * @return bool
	 */
	protected function checkPermanentFailure( $emailHeaders ) {
		if ( isset( $emailHeaders['status'] ) ) {
			$status = explode( '.', $emailHeaders['status'] );
			// According to RFC1893 status codes starting with 5 mean Permanent Failures
			return $status[0] == 5;
		} elseif ( isset( $emailHeaders['smtp-code'] ) ) {
			return $emailHeaders['smtp-code'] >= 500;
		} elseif ( isset( $emailHeaders['x-failed-recipients'] ) ) {
			// If not status code was found, let's presume that the presence of
			// X-Failed-Recipients means permanent failure
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Handle unrecognized bounces by notifying wiki admins with the full email
	 *
	 * @param string $email
	 * @param string $to
	 */
	public function handleUnrecognizedBounces( $email, $to ) {
		global $wgUnrecognizedBounceNotify, $wgPasswordSender;

		wfDebugLog( 'BounceHandler', "Received temporary bounce from $to" );
		$handleUnIdentifiedBounce = new ProcessUnRecognizedBounces(
			$wgUnrecognizedBounceNotify, $wgPasswordSender );
		$handleUnIdentifiedBounce->processUnRecognizedBounces( $email );
	}

	/**
	 * Get a lazy connection to the bounce table
	 *
	 * @param int $index DB_PRIMARY/DB_REPLICA
	 * @param string $wiki The DB that the bounced email was sent from
	 * @return IDatabase
	 */
	public static function getBounceRecordDB( $index, $wiki ) {
		global $wgBounceHandlerCluster, $wgBounceHandlerSharedDB;

		$wiki = $wgBounceHandlerSharedDB ?: $wiki;

		$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
		$lb = $wgBounceHandlerCluster
			? $lbFactory->getExternalLB( $wgBounceHandlerCluster )
			: $lbFactory->getMainLB( $wiki );

		return $lb->getConnection( $index, [], $wiki );
	}
}