View file upload/library/XenForo/Model/EmailBounce.php

File size: 7.99Kb
<?php

class XenForo_Model_EmailBounce extends XenForo_Model
{
	public function processBounceEmail($rawMessage)
	{
		$message = new Zend_Mail_Message(array('raw' => $rawMessage));

		$options = XenForo_Application::getOptions();
		$bounce = new XenForo_BounceParser($message,
			$options->enableVerp ? $options->bounceEmailAddress : null,
			XenForo_Application::getConfig()->globalSalt
		);

		$recipient = $bounce->getRecipient();
		if ($recipient)
		{
			$userId = $this->_getDb()->fetchOne('SELECT user_id FROM xf_user WHERE email = ?', $recipient);
			if (!$userId)
			{
				$userId = null;
			}
		}
		else
		{
			$userId = null;
		}

		$action = '';

		if ($userId && $bounce->isActionableBounce())
		{
			if ($bounce->recipientTrusted())
			{
				$details = $bounce->getStatusDetails();
				if ($details)
				{
					$action = $this->takeBounceAction($userId, $details['type'], $bounce->getMessageDate());
				}
			}
			else
			{
				$action = 'untrusted';
			}
		}

		return $this->_logBounceMessage($userId, $action, $bounce, $rawMessage);
	}

	public function takeBounceAction($userId, $bounceType, $bounceDate)
	{
		if (!$userId)
		{
			return '';
		}

		if ($bounceType == 'hard')
		{
			$this->triggerUserBounceAction($userId);

			return 'hard';
		}
		else if ($bounceType == 'soft')
		{
			$this->_getDb()->query("
				INSERT INTO xf_email_bounce_soft
					(user_id, bounce_date, bounce_total)
				VALUES
					(?, ?, 1)
				ON DUPLICATE KEY UPDATE
					bounce_total = bounce_total + 1
			", array($userId, gmdate('Y-m-d', $bounceDate)));

			if ($this->hasSoftBouncedTooMuch($userId))
			{
				$this->triggerUserBounceAction($userId);

				return 'soft_hard';
			}

			return 'soft';
		}

		return '';
	}

	public function triggerUserBounceAction($userId)
	{
		$user = XenForo_DataWriter::create('XenForo_DataWriter_User', XenForo_DataWriter::ERROR_SILENT);
		if ($user->setExistingData($userId))
		{
			if ($user->get('user_state') == 'valid'
				&& !$user->get('is_moderator')
				&& !$user->get('is_admin')
				&& !$user->get('is_staff')
			)
			{
				$user->set('user_state', 'email_bounce');
				$user->save();
			}
		}
	}

	protected function _logBounceMessage($userId, $action, XenForo_BounceParser $bounce, $rawMessage)
	{
		$this->_getDb()->insert('xf_email_bounce_log', array(
			'log_date' => XenForo_Application::$time,
			'email_date' => $bounce->getMessageDate(),
			'message_type' => $bounce->getMessageType(),
			'action_taken' => $action,
			'user_id' => $userId,
			'recipient' => $bounce->getRecipient(),
			'raw_message' => substr($rawMessage, 0, 1024 * 1024), // limit logging to 1MB to prevent potential insert issues
			'status_code' => $bounce->getStatusCode(),
			'diagnostic_info' => $bounce->getDiagnosticInfo()
		));
		return $this->_getDb()->lastInsertId();
	}

	public function logInvalidBounceMessage($rawMessage)
	{
		$this->_getDb()->insert('xf_email_bounce_log', array(
			'log_date' => XenForo_Application::$time,
			'email_date' => XenForo_Application::$time,
			'message_type' => 'unknown',
			'action_taken' => '',
			'raw_message' => substr($rawMessage, 0, 1024 * 1024), // limit logging to 1MB to prevent potential insert issues
		));
		return $this->_getDb()->lastInsertId();
	}

	public function getEmailBounceLogById($id)
	{
		return $this->_getDb()->fetchRow("
			SELECT *
			FROM xf_email_bounce_log
			WHERE bounce_id = ?
		", $id);
	}

	public function prepareEmailBounceConditions(array $conditions, array &$fetchOptions)
	{
		$sqlConditions = array();
		$db = $this->_getDb();

		if (!empty($conditions['message_type']))
		{
			$sqlConditions[] = 'email_bounce.message_type = ' . $db->quote($conditions['message_type']);
		}

		if (!empty($conditions['action_taken']))
		{
			$sqlConditions[] = 'email_bounce.action_taken = ' . $db->quote($conditions['action_taken']);
		}

		if (!empty($conditions['user_id']))
		{
			$sqlConditions[] = 'email_bounce.user_id = ' . $db->quote($conditions['user_id']);
		}

		if (!empty($conditions['recipient']))
		{
			$sqlConditions[] = 'email_bounce.recipient = ' . $db->quote($conditions['recipient']);
		}

		return $this->getConditionsForClause($sqlConditions);
	}

	public function getEmailBounceLogs(array $conditions = array(), array $fetchOptions = array())
	{
		$limitOptions = $this->prepareLimitFetchOptions($fetchOptions);
		$whereConditions = $this->prepareEmailBounceConditions($conditions, $fetchOptions);

		return $this->fetchAllKeyed($this->limitQueryResults(
			"
				SELECT email_bounce.*, user.*
				FROM xf_email_bounce_log AS email_bounce
				LEFT JOIN xf_user AS user ON (user.user_id = email_bounce.user_id)
				WHERE $whereConditions
				ORDER BY email_bounce.log_date DESC
			", $limitOptions['limit'], $limitOptions['offset']
		), 'bounce_id');
	}

	public function countEmailBounceLogs(array $conditions = array())
	{
		$fetchOptions = array();
		$whereConditions = $this->prepareEmailBounceConditions($conditions, $fetchOptions);

		return $this->_getDb()->fetchOne("
			SELECT COUNT(*)
			FROM xf_email_bounce_log AS email_bounce
			LEFT JOIN xf_user AS user ON (user.user_id = email_bounce.user_id)
			WHERE $whereConditions
		");
	}

	public function countRecentSoftBounces($userId, $numberOfDays)
	{
		$cutOffTimestamp = XenForo_Application::$time - intval($numberOfDays) * 86400;
		$cutOff = gmdate('Y-m-d', $cutOffTimestamp);

		$result = $this->_getDb()->fetchRow("
			SELECT COUNT(DISTINCT bounce_date) AS unique_days,
				DATEDIFF(MAX(bounce_date), MIN(bounce_date)) AS days_between,
				SUM(bounce_total) AS bounce_total
			FROM xf_email_bounce_soft
			WHERE user_id = ?
				AND bounce_date > ?
		", array($userId, $cutOff));

		if (!$result || !$result['unique_days'])
		{
			return array(
				'unique_days' => 0,
				'days_between' => 0,
				'bounce_total' => 0
			);
		}
		else
		{
			return $result;
		}
	}

	public function hasSoftBouncedTooMuch($userId)
	{
		$bounces = $this->countRecentSoftBounces($userId, 30);
		if (!$bounces['bounce_total'])
		{
			return false;
		}

		$thresholds = XenForo_Application::getOptions()->emailSoftBounceThreshold;
		return (
			$bounces['bounce_total'] >= $thresholds['bounce_total']
			&& $bounces['unique_days'] >= $thresholds['unique_days']
			&& $bounces['days_between'] >= $thresholds['days_between']
		);
	}

	public function pruneEmailBounceLogs($cutOff = null)
	{
		if ($cutOff === null)
		{
			$cutOff = XenForo_Application::$time - 86400 * 30;
		}

		$db = $this->_getDb();
		return $db->delete('xf_email_bounce_log', 'log_date < ' . $db->quote($cutOff));
	}

	public function pruneSoftBounceHistory($cutOff = null)
	{
		if ($cutOff === null)
		{
			$cutOff = XenForo_Application::$time - 86400 * 30;
		}
		$date = new DateTime("@$cutOff", new DateTimeZone('UTC'));
		$sqlDate = $date->format('Y-m-d');

		$db = $this->_getDb();
		return $db->delete('xf_email_bounce_soft', 'bounce_date < ' . $db->quote($sqlDate));
	}

	/**
	 * @return Zend_Mail_Storage_Abstract|null
	 *
	 * @throws Exception
	 */
	public function openBounceHandlerConnection()
	{
		if (!XenForo_Application::getOptions()->bounceEmailAddress)
		{
			return null;
		}

		$handler = XenForo_Application::getOptions()->emailBounceHandler;
		if (!$handler || empty($handler['enabled']))
		{
			return null;
		}

		$config = array(
			'host' => $handler['host'],
			'user' => $handler['username'],
			'password' => $handler['password']
		);
		if ($handler['port'])
		{
			$config['port'] = intval($handler['port']);
		}
		if ($handler['encryption'])
		{
			$config['ssl'] = strtoupper($handler['encryption']);
		}

		if ($handler['type'] == 'pop3')
		{
			$connection = new Zend_Mail_Storage_Pop3($config);
		}
		else if ($handler['type'] == 'imap')
		{
			$connection = new Zend_Mail_Storage_Imap($config);
		}
		else
		{
			throw new Exception("Unknown email handler $handler[type]");
		}

		return $connection;
	}
}