<?php
if ( ! defined( 'ABSPATH' ) ) {
	die( 'You are not allowed to call this page directly.' );
}

/**
 * @since 2.03.04
 */
class FrmEmail {

	/**
	 * @var string
	 */
	private $email_key = '';

	/**
	 * @var array
	 */
	private $to = array();

	/**
	 * @var array
	 */
	private $cc = array();

	/**
	 * @var array
	 */
	private $bcc = array();

	/**
	 * @var string
	 */
	private $from = '';

	/**
	 * @var string
	 */
	private $reply_to = '';

	/**
	 * @var string
	 */
	private $subject = '';

	/**
	 * @var string
	 */
	private $message = '';

	/**
	 * @var array
	 */
	private $attachments = array();

	/**
	 * @var bool
	 */
	private $is_plain_text = false;

	/**
	 * @var bool
	 */
	private $is_single_recipient = false;

	/**
	 * @var bool
	 */
	private $include_user_info = false;

	/**
	 * @var string
	 */
	private $charset = '';

	/**
	 * @var string
	 */
	private $content_type = 'text/html';

	/**
	 * @var array
	 */
	private $settings = array();

	/**
	 * @var stdClass
	 */
	private $entry;

	/**
	 * @var stdClass
	 */
	private $form;

	/**
	 * @var int
	 */
	private $action_id = 0;

	/**
	 * FrmEmail constructor
	 *
	 * @param object $action
	 * @param object $entry
	 * @param object $form
	 */
	public function __construct( $action, $entry, $form ) {
		$this->set_email_key( $action );
		$this->entry     = $entry;
		$this->form      = $form;
		$this->settings  = $action->post_content;
		$this->action_id = (int) $action->ID;

		$user_id_args = self::get_user_id_args( $form->id );
		$this->set_to( $user_id_args );
		$this->set_cc( $user_id_args );
		$this->set_bcc( $user_id_args );

		if ( ! $this->has_recipients() ) {
			return;
		}

		$this->set_from( $user_id_args );
		$this->set_reply_to( $user_id_args );

		$this->set_include_user_info();
		$this->set_is_plain_text();
		$this->set_is_single_recipient( $action );

		$this->set_charset();
		$this->set_content_type();

		$this->set_subject();
		$this->set_message();
		$this->set_attachments();
	}

	/**
	 * Set the email key property
	 *
	 * @since 2.03.04
	 *
	 * @param object $action
	 *
	 * @return void
	 */
	private function set_email_key( $action ) {
		$this->email_key = $action->ID;
	}

	/**
	 * Set the to addresses
	 *
	 * @since 2.03.04
	 *
	 * @param array $user_id_args
	 *
	 * @return void
	 */
	private function set_to( $user_id_args ) {
		$to = $this->prepare_email_setting( $this->settings['email_to'], $user_id_args );
		$to = $this->explode_emails( $to );

		$where  = array(
			'it.field_id !' => 0,
			'it.item_id'    => $this->entry->id,
		);
		$values = FrmEntryMeta::getAll( $where, ' ORDER BY fi.field_order' );
		$args   = array(
			'email_key' => $this->email_key,
			'entry'     => $this->entry,
			'form'      => $this->form,
		);
		$to     = apply_filters( 'frm_to_email', $to, $values, $this->form->id, $args );

		$this->to = array_unique( (array) $to );

		if ( ! $this->to ) {
			return;
		}

		$this->handle_phone_numbers();

		$this->to = $this->format_recipients( $this->to );
	}

	/**
	 * Set the CC addresses
	 *
	 * @since 2.03.04
	 *
	 * @param array $user_id_args
	 *
	 * @return void
	 */
	private function set_cc( $user_id_args ) {
		$this->cc = $this->prepare_additional_recipients( $this->settings['cc'], $user_id_args );
	}

	/**
	 * Set the BCC addresses
	 *
	 * @since 2.03.04
	 *
	 * @param array $user_id_args
	 *
	 * @return void
	 */
	private function set_bcc( $user_id_args ) {
		$this->bcc = $this->prepare_additional_recipients( $this->settings['bcc'], $user_id_args );
	}

	/**
	 * Prepare CC and BCC recipients
	 *
	 * @since 2.03.04
	 *
	 * @param string $recipients
	 * @param array  $user_id_args
	 *
	 * @return array
	 */
	private function prepare_additional_recipients( $recipients, $user_id_args ) {
		$recipients = $this->prepare_email_setting( $recipients, $user_id_args );
		$recipients = $this->explode_emails( $recipients );
		$recipients = array_unique( (array) $recipients );

		return $this->format_recipients( $recipients );
	}

	/**
	 * Set the From addresses
	 *
	 * @since 2.03.04
	 *
	 * @param array $user_id_args
	 *
	 * @return void
	 */
	private function set_from( $user_id_args ) {
		if ( empty( $this->settings['from'] ) ) {
			$from = FrmEmailHelper::get_default_from_email();
		} else {
			$from = $this->prepare_email_setting( $this->settings['from'], $user_id_args );
		}

		$this->from = $this->format_from( $from );
	}

	/**
	 * Set the Reply To addresses
	 *
	 * @since 2.03.04
	 *
	 * @param array $user_id_args
	 *
	 * @return void
	 */
	private function set_reply_to( $user_id_args ) {
		$this->reply_to = trim( $this->settings['reply_to'] );

		if ( $this->reply_to ) {
			$this->reply_to = $this->prepare_email_setting( $this->settings['reply_to'], $user_id_args );
		}

		if ( ! $this->reply_to ) {
			$this->reply_to = $this->get_email_from_name( $this->from );
		}

		$this->reply_to = $this->format_reply_to( $this->reply_to );
	}

	/**
	 * Set the is_plain_text property
	 * This should be set before the message
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function set_is_plain_text() {
		if ( empty( $this->settings['email_style'] ) ) {
			// If `email_style` isn't set, use the `plain_text` checkbox.
			$this->is_plain_text = ! empty( $this->settings['plain_text'] );
			return;
		}

		$this->is_plain_text = 'plain' === $this->settings['email_style'];
	}

	/**
	 * Set the include_user_info property
	 * This should be set before the message
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function set_include_user_info() {
		if ( isset( $this->settings['inc_user_info'] ) ) {
			$this->include_user_info = $this->settings['inc_user_info'];
		}
	}

	/**
	 * Set the is_single_recipient property
	 *
	 * @since 2.03.04
	 *
	 * @param object $action
	 *
	 * @return void
	 */
	private function set_is_single_recipient( $action ) {
		$args = array(
			'form'   => $this->form,
			'entry'  => $this->entry,
			'action' => $action,
		);

		/**
		 * Send a separate email for email address in the "to" section
		 *
		 * @since 2.2.13
		 */
		$this->is_single_recipient = apply_filters( 'frm_send_separate_emails', false, $args );
	}

	/**
	 * Set the charset
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function set_charset() {
		$this->charset = get_option( 'blog_charset' );
	}

	/**
	 * Set the content type
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function set_content_type() {
		if ( $this->is_plain_text ) {
			$this->content_type = 'text/plain';
		}
	}

	/**
	 * Set the subject
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function set_subject() {
		if ( empty( $this->settings['email_subject'] ) ) {
			/* translators: %1$s: Form name, %2$s: Site name */
			$this->subject = sprintf( __( '%1$s Form submitted on %2$s', 'formidable' ), $this->form->name, '[sitename]' );
		} else {
			$this->subject = $this->settings['email_subject'];
		}

		// This also replaces [sitename] shortcode in default.
		$this->subject = FrmFieldsHelper::basic_replace_shortcodes( $this->subject, $this->form, $this->entry );

		$args          = array(
			'form'      => $this->form,
			'entry'     => $this->entry,
			'email_key' => $this->email_key,
		);
		$this->subject = apply_filters( 'frm_email_subject', $this->subject, $args );
		$this->subject = wp_specialchars_decode( strip_tags( stripslashes( $this->subject ) ), ENT_QUOTES );
	}

	/**
	 * Set the email message
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function set_message() {
		$this->message = $this->settings['email_message'];

		if ( ! $this->is_plain_text ) {
			// The decode is to support [default-html] shortcodes.
			$this->message = html_entity_decode( $this->message );
		}

		$this->message  = FrmFieldsHelper::basic_replace_shortcodes( $this->message, $this->form, $this->entry );
		$prev_mail_body = $this->message;

		// Make a copy to prevent changes by reference.
		$pass_entry = clone $this->entry;
		$mail_body  = FrmEntriesHelper::replace_default_message(
			$prev_mail_body,
			array(
				'id'          => $this->entry->id,
				'entry'       => $pass_entry,
				'plain_text'  => $this->is_plain_text,
				'user_info'   => $this->include_user_info,
				'table_style' => $this->settings['email_style'],
			)
		);

		// Add the user info if it isn't already included
		if ( $this->include_user_info && $prev_mail_body === $mail_body ) {
			$data       = $this->entry->description;
			$mail_body .= "\r\n\r\n" . __( 'User Information', 'formidable' ) . "\r\n";
			$this->maybe_add_ip( $mail_body );
			$mail_body .= __( 'User-Agent (Browser/OS)', 'formidable' ) . ': ' . FrmEntriesHelper::get_browser( $data['browser'] ) . "\r\n";
			$mail_body .= __( 'Referrer', 'formidable' ) . ': ' . $data['referrer'] . "\r\n";
		}

		$this->message = $mail_body;

		if ( $this->is_plain_text ) {
			$this->message = wp_specialchars_decode( strip_tags( $this->message ), ENT_QUOTES );
			$this->message = str_replace( '&nbsp;', '', $this->message );
		} else {
			$this->add_autop();
		}

		$this->message = apply_filters( 'frm_email_message', $this->message, $this->package_atts() );
	}

	/**
	 * Runs message through autop, extracting the content inside body tag if it has <body>.
	 *
	 * @return void
	 */
	private function add_autop() {
		$message = $this->message;
		preg_match( '/<body[^>]*>([\s\S]*?)<\/body>/', $message, $match );

		if ( ! empty( $match[1] ) ) {
			$this->message = str_replace( $match[1], trim( wpautop( $match[1] ) ), $message );
		} else {
			$this->message = trim( wpautop( $message ) );
		}
	}

	/**
	 * @param string $mail_body
	 *
	 * @return void
	 */
	private function maybe_add_ip( &$mail_body ) {
		if ( ! empty( $this->entry->ip ) ) {
			$mail_body .= __( 'IP Address', 'formidable' ) . ': ' . $this->entry->ip . "\r\n";
		}
	}

	/**
	 * Set the attachments for an email message
	 *
	 * @since 2.03.04
	 * @since 5.0.16 added new action_id key to $args.
	 *
	 * @return void
	 */
	private function set_attachments() {
		$args = array(
			'entry'     => $this->entry,
			'email_key' => $this->email_key,
			'settings'  => $this->settings,
			'action_id' => $this->action_id,
		);

		$this->attachments = apply_filters( 'frm_notification_attachment', array(), $this->form, $args );
	}

	/**
	 * Check if an email should send
	 *
	 * @since 2.03.04
	 *
	 * @return bool|mixed|void
	 */
	public function should_send() {
		if ( ! $this->has_recipients() ) {
			return false;
		}

		$filter_args = array(
			'message'   => $this->message,
			'subject'   => $this->subject,
			'recipient' => $this->to,
			'header'    => $this->package_header(),
		);
		/**
		 * Stop an email based on the message, subject, recipient,
		 * or any information included in the email header
		 *
		 * @since 2.2.8
		 */
		return apply_filters( 'frm_send_email', true, $filter_args );
	}

	/**
	 * Check if an email has any recipients
	 *
	 * @since 2.03.04
	 *
	 * @return bool
	 */
	private function has_recipients() {
		return $this->to || $this->cc || $this->bcc;
	}

	/**
	 * Send an email
	 *
	 * @since 2.03.04
	 *
	 * @return bool
	 */
	public function send() {
		$this->remove_buddypress_filters();
		$this->add_mandrill_filter();

		$sent = false;

		if ( count( $this->to ) > 1 && $this->is_single_recipient ) {
			foreach ( $this->to as $recipient ) {
				$sent = $this->send_single( $recipient );
			}
		} else {
			$sent = $this->send_single( $this->to );
		}

		$this->remove_mandrill_filter();

		return $sent;
	}

	/**
	 * Send a single email
	 *
	 * @since 2.03.04
	 *
	 * @param array|string $recipient
	 *
	 * @return bool
	 */
	private function send_single( $recipient ) {
		$header = apply_filters(
			'frm_email_header',
			$this->package_header(),
			array(
				'to_email' => $recipient,
				'subject'  => $this->subject,
			)
		);

		$subject = $this->encode_subject( $this->subject );
		$sent    = wp_mail( $recipient, $subject, $this->message, $header, $this->attachments );

		if ( ! $sent ) {
			if ( is_array( $header ) ) {
				$header = implode( "\r\n", $header );
			}

			$recipient = implode( ',', (array) $recipient );
			$sent      = mail( $recipient, $subject, $this->message, $header );
		}

		do_action( 'frm_notification', $recipient, $subject, $this->message );

		return $sent;
	}

	/**
	 * Package the email header
	 *
	 * @since 2.03.04
	 *
	 * @return array
	 */
	private function package_header() {
		$header = array();

		if ( $this->cc ) {
			$header[] = 'CC: ' . implode( ',', $this->cc );
		}

		if ( $this->bcc ) {
			$header[] = 'BCC: ' . implode( ',', $this->bcc );
		}

		$header[] = 'From: ' . $this->from;
		$header[] = 'Reply-To: ' . $this->reply_to;
		$header[] = 'Content-Type: ' . $this->content_type . '; charset="' . esc_attr( $this->charset ) . '"';

		return $header;
	}

	/**
	 * Get the userID field ID and key for email settings
	 *
	 * @since 2.03.04
	 *
	 * @param int $form_id
	 *
	 * @return array
	 */
	private function get_user_id_args( $form_id ) {
		$user_id_args = array(
			'field_id'  => '',
			'field_key' => '',
		);

		$user_id_args['field_id'] = FrmEmailHelper::get_user_id_field_for_form( $form_id );

		if ( $user_id_args['field_id'] ) {
			$user_id_args['field_key'] = FrmField::get_key_by_id( $user_id_args['field_id'] );
		}

		return $user_id_args;
	}

	/**
	 * Prepare the to, cc, bcc, reply_to, and from setting
	 *
	 * @since 2.03.04
	 *
	 * @param string $value
	 * @param array  $user_id_args
	 *
	 * @return string
	 */
	private function prepare_email_setting( $value, $user_id_args ) {
		if ( str_contains( $value, '[' . $user_id_args['field_id'] . ']' ) ) {
			$value = str_replace( '[' . $user_id_args['field_id'] . ']', '[' . $user_id_args['field_id'] . ' show="user_email"]', $value );
		} elseif ( str_contains( $value, '[' . $user_id_args['field_key'] . ']' ) ) {
			$value = str_replace( '[' . $user_id_args['field_key'] . ']', '[' . $user_id_args['field_key'] . ' show="user_email"]', $value );
		}

		$value = FrmFieldsHelper::basic_replace_shortcodes( $value, $this->form, $this->entry );

		// Remove brackets and add a space in case there isn't one
		$value = str_replace( '<', ' ', $value );

		return str_replace( array( '"', '>' ), '', $value );
	}

	/**
	 * Extract the emails from cc and bcc. Allow separation by , or ;.
	 * Trim the emails here as well
	 *
	 * @since 2.03.04
	 *
	 * @param string $emails
	 *
	 * @return array|string Emails.
	 */
	private function explode_emails( $emails ) {
		$emails = $emails ? preg_split( '/(,|;)/', $emails ) : '';
		return is_array( $emails ) ? array_map( 'trim', $emails ) : trim( $emails );
	}

	/**
	 * Format the recipients( to, cc, bcc)
	 *
	 * @param array $recipients
	 *
	 * @return array
	 */
	private function format_recipients( $recipients ) {
		if ( ! $recipients ) {
			return $recipients;
		}

		foreach ( $recipients as $key => $val ) {
			$val = trim( $val );

			if ( is_email( $val ) ) {
				// If a plain email is used, no formatting is needed
				continue;
			}

			$parts = explode( ' ', $val );
			$email = end( $parts );

			if ( ! is_email( $email ) ) {
				// If user enters a name without an email
				unset( $recipients[ $key ] );
				continue;
			}

			// If user enters a name and email
			$name = trim( str_replace( $email, '', $val ) );

			$recipients[ $key ] = $this->format_from_email( $name, $email );
		}//end foreach

		return $recipients;
	}

	/**
	 * Format the From header
	 *
	 * @param string $from
	 *
	 * @return string
	 */
	private function format_from( $from ) {
		$from = trim( $from );

		if ( is_email( $from ) ) {
			// If a plain email is used, add the site name so "WordPress" doesn't get added
			$from_name  = wp_specialchars_decode( FrmAppHelper::site_name(), ENT_QUOTES );
			$from_email = $from;
		} else {
			list( $from_name, $from_email ) = $this->get_name_and_email_for_sender( $from );
		}

		// If sending the email from a yahoo address, change it to the WordPress default
		if ( str_contains( $from_email, '@yahoo.com' ) ) {
			// Get the site domain and get rid of www.
			$sitename = strtolower( FrmAppHelper::get_server_value( 'SERVER_NAME' ) );

			if ( str_starts_with( $sitename, 'www.' ) ) {
				$sitename = substr( $sitename, 4 );
			}

			$from_email = 'wordpress@' . $sitename;
		}

		return $this->format_from_email( $from_name, $from_email );
	}

	/**
	 * Format the Reply To property
	 *
	 * @since 2.03.04
	 *
	 * @param string $reply_to
	 *
	 * @return string
	 */
	private function format_reply_to( $reply_to ) {
		$reply_to = trim( $reply_to );

		if ( ! is_email( $reply_to ) ) {
			list( $name, $email ) = $this->get_name_and_email_for_sender( $reply_to );
			$reply_to             = $this->format_from_email( $name, $email );
		}

		return $reply_to;
	}

	/**
	 * Get only the email if the name and email have been combined
	 *
	 * @since 3.0.06
	 *
	 * @param string $name
	 *
	 * @return string
	 */
	private function get_email_from_name( $name ) {
		$email = trim( trim( $name, '>' ), '<' );

		if ( str_contains( $email, '<' ) ) {
			$parts = explode( '<', $email );
			$email = trim( $parts[1], '>' );
		}

		return $email;
	}

	/**
	 * Get the name and email for the From or Reply To header
	 *
	 * @since 2.03.04
	 *
	 * @param string $sender
	 *
	 * @return array
	 */
	private function get_name_and_email_for_sender( $sender ) {
		$parts = explode( ' ', $sender );
		$end   = end( $parts );

		if ( is_email( $end ) ) {
			$name = trim( str_replace( $end, '', $sender ) );
		} else {
			// Only a name was entered in the From or Reply To field
			$name = $sender;
			$end  = get_option( 'admin_email' );
		}

		return array( $name, $end );
	}

	/**
	 * @since 3.0.06
	 *
	 * @param string $name
	 * @param string $email
	 *
	 * @return string
	 */
	private function format_from_email( $name, $email ) {
		return '' !== $name ? $name . ' <' . $email . '>' : $email;
	}

	/**
	 * Remove phone numbers from To addresses
	 * Send the phone numbers to the frm_send_to_not_email hook
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function handle_phone_numbers() {
		foreach ( $this->to as $key => $recipient ) {
			if ( '[admin_email]' === $recipient || is_email( $recipient ) ) {
				continue;
			}

			$recipient = explode( ' ', $recipient );

			if ( is_email( end( $recipient ) ) ) {
				continue;
			}

			do_action(
				'frm_send_to_not_email',
				array(
					'e'           => $recipient,
					'subject'     => $this->subject,
					'mail_body'   => $this->message,
					'reply_to'    => $this->reply_to,
					'from'        => $this->from,
					'plain_text'  => $this->is_plain_text,
					'attachments' => $this->attachments,
					'form'        => $this->form,
					'email_key'   => $key,
				)
			);

			// Remove phone number from to addresses
			unset( $this->to[ $key ] );
		// end if
		}//end foreach
	}

	/**
	 * Package an array of FrmEmail properties
	 *
	 * @since 2.03.04
	 *
	 * @return array
	 */
	public function package_atts() {
		return array(
			'to_email'    => $this->to,
			'cc'          => $this->cc,
			'bcc'         => $this->bcc,
			'from'        => $this->from,
			'reply_to'    => $this->reply_to,
			'subject'     => $this->subject,
			'message'     => $this->message,
			'attachments' => $this->attachments,
			'plain_text'  => $this->is_plain_text,
			'email_key'   => $this->email_key,
			'form'        => $this->form,
			'entry'       => $this->entry,
			'email_style' => $this->settings['email_style'],
		);
	}

	/**
	 * Remove the Buddypress email filters
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function remove_buddypress_filters() {
		remove_filter( 'wp_mail_from', 'bp_core_email_from_address_filter' );
		remove_filter( 'wp_mail_from_name', 'bp_core_email_from_name_filter' );
	}

	/**
	 * Add Mandrill line break filter
	 * Remove line breaks in HTML emails to prevent conflicts with Mandrill
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function add_mandrill_filter() {
		if ( ! $this->is_plain_text ) {
			add_filter( 'mandrill_nl2br', 'FrmEmailHelper::remove_mandrill_br' );
		}
	}

	/**
	 * Remove Mandrill line break filter
	 *
	 * @since 2.03.04
	 *
	 * @return void
	 */
	private function remove_mandrill_filter() {
		remove_filter( 'mandrill_nl2br', 'FrmEmailHelper::remove_mandrill_br' );
	}

	/**
	 * Encode the email subject
	 *
	 * @param string $subject
	 *
	 * @return string
	 */
	private function encode_subject( $subject ) {
		if ( apply_filters( 'frm_encode_subject', false, $subject ) ) {
			return '=?' . $this->charset . '?B?' . base64_encode( $subject ) . '?=';
		}

		return $subject;
	}
}
