HEX
Server: nginx/1.18.0
System: Linux iZj6c1ieg2jrpk1z5tzi19Z 6.3.9-1.el7.elrepo.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Jun 21 22:18:40 EDT 2023 x86_64
User: www (1001)
PHP: 8.2.4
Disabled: passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv
Upload Files
File: /www/wwwroot/www.cytocare.cn/wp-content/plugins/site-mailer/modules/mailer/classes/mail-handler.php
<?php

namespace SiteMailer\Modules\Mailer\Classes;

use Exception;
use SiteMailer\Classes\Database\Exceptions\Missing_Table_Exception;
use SiteMailer\Classes\Logger;
use SiteMailer\Classes\Services\Client;
use SiteMailer\Modules\Logs\Database\Log_Entry;
use SiteMailer\Modules\Logs\Database\Logs_Table;
use SiteMailer\Modules\Mailer\Components\Rate_Limit_Retry;
use SiteMailer\Modules\Settings\Classes\Settings as SettingsModule;
use SiteMailer\Modules\Settings\Module as Settings;
use SiteMailer\Modules\Statuses\Database\Status_Entry;
use SiteMailer\Modules\Statuses\Database\Statuses_Table;
use Throwable;

/**
 * The class is responsible for the send email to external service
 */
class Mail_Handler {


	const SERVICE_ENDPOINT = 'email-account/send';
	const LOG_STATUSES = [
		'pending' => 'pending',
		'failed' => 'failed',
		'not_sent' => 'not sent',
		'rate_limit' => 'rate limit',
		'not_valid' => 'not valid',
		'unsubscribed' => 'unsubscribed',
	];
	const ERROR_MSG = [
		'rate_limit' => 'Too many requests',
		'not_valid' => 'Email `To` or `message` empty',
		'unsubscribed' => 'All recipients from "To" list are unsubscribed',
		'not_sent' => 'Quota Status Guard Request Failed!: Quota exceeded',
	];

	private array $email;
	private array $initial_email;
	private string $log_id;
	private array $attachments = [];
	private string $source;
	private string $type;
	private array $unsubscribed = [];

	/**
	 * Get data from logs and try to send one more time
	 *
	 * @param array $ids
	 *
	 * @return void
	 * @throws Throwable
	 */
	public static function resend_mails( array $ids ): void {
		$escaped = implode( ',', array_map( function ( $item ) {
			return Logs_Table::db()->prepare( '%s', $item );
		}, $ids ) );
		$where = Logs_Table::table_name() . '.' . Logs_Table::API_ID . ' IN (' . $escaped . ')';
		$logs = Log_Entry::get_logs( $where );
		// TODO: Discuss and add possibility to resend mails as array with one request
		foreach ( $logs as $log ) {
			$log->to = json_decode( $log->to, true );
			$log->headers = json_decode( $log->headers, true );
			$handler = new self( (array) $log, 'Resend', 'Plugin' );
			$handler->send();
		}
	}

	/**
	 * Create and send test mail
	 *
	 * @param string $address
	 *
	 * @return void
	 * @throws Throwable
	 */
	public static function send_test_mail( string $address ): void {
		$current_timestamp = current_time( 'mysql' );
		$url = get_bloginfo( 'url' );
		/* translators: %s is the timestamp */
		$msg = '<!doctype html>
				<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
					<head>
						<title>' . __( 'Site Mailer Email Test', 'site-mailer' ) . '</title>
					</head>
					<body>
						<p style="padding: 4px 0">' . __( 'Congrats, the test email was sent successfully!', 'site-mailer' ) . '</p>
						<p style="padding: 4px 0">' . __( 'Thank you for using Site Mailer. We are here to make sure your emails actually get delivered!', 'site-mailer' ) . '</p>
						<p style="padding: 12px 0">' . __( 'The Site Mailer Team', 'site-mailer' ) . '</p>
						<p style="padding: 4px 0">' . __( 'Sent By:', 'site-mailer' ) . ' ' . $url . '</p>
						<p style="padding: 4px 0">' . __( 'Timestamp:', 'site-mailer' ) . ' ' . $current_timestamp . '</p>
					</body>
				</html>';
		$email = [
			'to' => $address,
			'subject' => __( 'Site Mailer Email Test', 'site-mailer' ),
			'message' => $msg,
			'headers' => 'Content-Type: text/html',
		];
		$handler = new self( $email, 'Test', 'Plugin' );
		$handler->send();
	}

	/**
	 * Get unsubscribe endpoint
	 *
	 * @param $to string
	 *
	 * @return string
	 */
	private function get_unsubscribe_endpoint( string $to ): string {
		$blog_id = get_current_blog_id();
		$data = Settings::get_unsubscribe_encryption_data();
		$email = openssl_encrypt( $to, $data['cipher'], $data['key'], 0, base64_decode( $data['iv'] ) );
		$path = 'site-mailer/v1/suppressions/unsubscribe/' . base64_encode( $email );

		return get_rest_url( $blog_id, $path );
	}

	/**
	 * Add data from settings to email, remove unsubscribed from list of recipients
	 *
	 * @return void
	 * @throws Exception
	 */
	private function prepare_data() {
		$reply_to = Settings::get_sender_reply_email();

		if ( SettingsModule::get( SettingsModule::UNSUBSCRIBE ) ) {
			$unsubscribe = [];
			if ( function_exists( 'openssl_encrypt' ) ) {
				foreach ( $this->email['to'] as $item ) {
					$unsubscribe[ $item ] = [
						'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click',
						'List-Unsubscribe' => '<mailto:' . $reply_to . '>, <' . $this->get_unsubscribe_endpoint( $item ) . '>',
					];
				}
				$this->email['headers'][] = 'Unsubscribe: ' . wp_json_encode( $unsubscribe );
			} else {
				Logger::error( 'the openssl extension is not installed in the environment' );
			}
		}

		$this->email['to'] = implode( ', ', $this->email['to'] );
		$this->email = array_merge( $this->email, [
			'from_name' => Settings::get_sender_from_name(),
			'reply_to' => $reply_to,
		] );
	}

	/**
	 * get_mail_attachments
	 *
	 * Get file content and path info from tmp file
	 *
	 * TODO add store file if needed
	 */
	private function get_mail_attachments(): void {
		if ( array_key_exists( 'attachments', $this->email ) && ! empty( $this->email['attachments'] ) ) {
			foreach ( $this->email['attachments'] as $attachment ) {
				$file = file_get_contents( $attachment ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reason: Using file_get_contents for get an file from instance.
				$pathinfo = pathinfo( $attachment );
				$this->attachments[] = [
					'file' => base64_encode( $file ),
					// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Reason: Encoding data for REST call.
					'basename' => $pathinfo['basename'],
					'type' => mime_content_type( $attachment ),
				];
			}
		}
	}

	/**
	 * Check 'To' and 'message' for empty or wrong
	 * @throws Throwable
	 */
	private function check_mail() {
		$data = Mail_Validator::get_instance()->validate( $this->email );
		$this->email = $data['email'];
		$this->unsubscribed = $data['unsubscribed'];
		if ( count( $data['unsubscribed'] ) && empty( $this->email['to'] ) && empty( array_diff( $data['to'], $data['unsubscribed'] ) ) ) {
			$this->error_handler( self::ERROR_MSG['unsubscribed'] );
		}

		if ( empty( $this->email['to'] ) || empty( $this->email['message'] ) ) {
			$this->error_handler( self::ERROR_MSG['not_valid'] );
		}
	}

	/**
	 * Create and save log entry
	 *
	 * @param string|null $status
	 *
	 * @return void
	 * @throws Missing_Table_Exception
	 */
	private function write_log( string $status = null ) {
		$keep_log = SettingsModule::get( SettingsModule::KEEP_LOG );
		$datetime_wp = current_time( 'mysql' );
		$mail_list = Mail_Validator::get_instance()->validate_for_log_status( $this->initial_email, $this->unsubscribed );

		foreach ( $mail_list as $email => $item ) {
			$status_item = new Status_Entry( [
				'data' => [
					Statuses_Table::LOG_ID => $this->log_id,
					Statuses_Table::EMAIL => $email,
					Statuses_Table::STATUS => $item['status'] ? $item['status'] : ( $status ?? self::LOG_STATUSES['pending'] ),
					Statuses_Table::TYPE => $item['type'],
					Statuses_Table::CREATED_AT => $datetime_wp,
					Statuses_Table::UPDATED_AT => $datetime_wp,
				],
			] );
			$status_item->create();
		}

		$required = [
			Logs_Table::API_ID => $this->log_id,
			Logs_Table::SUBJECT => $this->email['subject'],
			// possible array of strings
			Logs_Table::TO => wp_json_encode( $this->initial_email['to'] ),
			// possible array of strings
			Logs_Table::HEADERS => wp_json_encode( array_key_exists( 'headers', $this->initial_email ) ? $this->initial_email['headers'] : '' ),
			Logs_Table::SOURCE => $this->source,
			Logs_Table::CREATED_AT => $datetime_wp,
			Logs_Table::UPDATED_AT => $datetime_wp,
		];
		$on_keep = $keep_log ? [
			Logs_Table::MESSAGE => $this->email['message'],
		] : [];
		$log = new Log_Entry( [ 'data' => array_merge( $required, $on_keep ) ] );
		$log->create();
	}

	/**
	 * Handle send email error
	 *
	 * @param string $error
	 *
	 * @throws Missing_Table_Exception
	 * @throws Exception
	 */
	private function error_handler( string $error ) {
		switch ( $error ) {
			case self::ERROR_MSG['not_sent']:
				$status = self::LOG_STATUSES['not_sent'];
				break;
			case self::ERROR_MSG['rate_limit']:
				$status = self::LOG_STATUSES['rate_limit'];
				Rate_Limit_Retry::schedule_resend_email( $this->log_id, $this->email );
				break;
			case self::ERROR_MSG['not_valid']:
				$status = self::LOG_STATUSES['not_valid'];
				break;
			case self::ERROR_MSG['unsubscribed']:
				$status = self::LOG_STATUSES['unsubscribed'];
				break;
			default:
				$status = self::LOG_STATUSES['failed'];
				break;
		}

		$this->write_log( $status );

		// Don't throw an error on 'rate_limit'
		if ( self::ERROR_MSG['rate_limit'] !== $error ) {
			throw new Exception( esc_html( $error ) );
		}
	}

	/**
	 * send request with mail to the api service
	 *
	 * Send mail to the external service
	 *
	 * @return void
	 * @throws Missing_Table_Exception
	 * @throws Exception
	 */
	public function send() {
		$response = Client::get_instance()->make_request(
			'POST',
			self::SERVICE_ENDPOINT,
			[
				'email' => $this->email,
				'attachments' => $this->attachments,
				'from' => Settings::get_sender_email(),
				'custom_args' => [
					'email_id' => $this->log_id,
					'source' => $this->source,
					'type' => $this->type,
					'status' => self::LOG_STATUSES['pending'],
				],
			],
			[],
			true
		);
		if ( is_wp_error( $response ) ) {
			$error = $response->get_error_message();
			$this->error_handler( $error );
		}

		$this->write_log();
	}

	/**
	 *
	 * @param array $email Array of the `wp_mail()` arguments.
	 * @param string $type Normal|Resend|Test
	 * @param string|null $source
	 *
	 * @throws Throwable
	 */
	public function __construct( array $email, string $type, string $source = null ) {
		$this->log_id = wp_generate_uuid4();
		$this->source = $source ?? Caller_Source::get_caller_source();
		$this->type = $type;
		$this->initial_email = $email;
		$this->email = $email;
		$this->check_mail();
		$this->prepare_data();
		$this->get_mail_attachments();
	}
}