This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,120 @@
<?php
namespace Gravity_Forms\Gravity_SMTP\Handler;
use Gravity_Forms\Gravity_SMTP\Apps\App_Service_Provider;
use Gravity_Forms\Gravity_SMTP\Connectors\Connector_Service_Provider;
use Gravity_Forms\Gravity_SMTP\Feature_Flags\Feature_Flag_Manager;
use Gravity_Forms\Gravity_SMTP\Handler\Config\Handler_Endpoints_Config;
use Gravity_Forms\Gravity_SMTP\Handler\Endpoints\Resend_Email_Endpoint;
use Gravity_Forms\Gravity_SMTP\Handler\External\Gravity_Forms_Note_Handler;
use Gravity_Forms\Gravity_SMTP\Logging\Logging_Service_Provider;
use Gravity_Forms\Gravity_SMTP\Models\Event_Model;
use Gravity_Forms\Gravity_SMTP\Suppression\Suppression_Service_Provider;
use Gravity_Forms\Gravity_Tools\Providers\Config_Service_Provider;
use Gravity_Forms\Gravity_Tools\Service_Container;
use Gravity_Forms\Gravity_Tools\Utils\Utils_Service_Provider;
class Handler_Service_Provider extends Config_Service_Provider {
const HANDLER = 'mail_handler';
const HANDLER_ENDPOINTS_CONFIG = 'handler_endpoints_config';
const NOTE_HANDLER = 'note_handler';
const RESEND_EMAIL_ENDPOINT = 'resend_email_endpoint';
protected $configs = array(
self::HANDLER_ENDPOINTS_CONFIG => Handler_Endpoints_Config::class,
);
public function register( Service_Container $container ) {
parent::register( $container );
$container->add( self::HANDLER, function () use ( $container ) {
$factory = $container->get( Connector_Service_Provider::CONNECTOR_FACTORY );
$data_store = $container->get( Connector_Service_Provider::DATA_STORE_ROUTER );
$source_parser = $container->get( Utils_Service_Provider::SOURCE_PARSER );
$suppressed_model = $container->get( Suppression_Service_Provider::SUPPRESSED_EMAILS_MODEL );
return new Mail_Handler( $factory, $data_store, $source_parser, $suppressed_model );
} );
$container->add( self::RESEND_EMAIL_ENDPOINT, function () use ( $container ) {
return new Resend_Email_Endpoint( $container->get( Connector_Service_Provider::EVENT_MODEL ), $container->get( Logging_Service_Provider::DEBUG_LOGGER ), $container->get( Utils_Service_Provider::ATTACHMENTS_SAVER ) );
} );
$container->add( self::NOTE_HANDLER, function() {
return new Gravity_Forms_Note_Handler();
} );
}
public function init( Service_Container $container ) {
add_action( 'wp_ajax_' . Resend_Email_Endpoint::ACTION_NAME, function () use ( $container ) {
$container->get( self::RESEND_EMAIL_ENDPOINT )->handle();
} );
if ( ! Feature_Flag_Manager::is_enabled( 'gravityforms_entry_note' ) ) {
return;
}
$is_configured = Mail_Handler::is_minimally_configured();
$test_mode = Mail_Handler::is_test_mode();
if ( ! $is_configured && ! $test_mode ) {
return;
}
/**
* @var Mail_Handler $handler
*/
$handler = $container->get( self::HANDLER );
/**
* @var Event_Model $model
*/
$model = $container->get( Connector_Service_Provider::EVENT_MODEL );
/**
* @var Gravity_Forms_Note_Handler $note_handler
*/
$note_handler = $container->get( self::NOTE_HANDLER );
add_filter( 'gform_pre_send_email', function( $email_args, $format, $notification, $entry ) use ( $handler, $note_handler ) {
$note_handler->store_id( $entry['id'], $handler );
return $email_args;
}, 99, 4 );
add_filter( 'gform_notification_note', function ( $note_args, $entry_id, $result ) use ( $handler, $model, $note_handler ) {
if ( $result !== true ) {
return $note_args;
}
$note_args['text'] = $note_handler->get_modified_entry_note( $note_args['text'], $entry_id, $handler, $model );
return $note_args;
}, 10, 3 );
// When an email fails to send, clear the configuration cache for oauth providers to ensure the UI reflects any errors.
add_action( 'gravitysmtp_on_send_failure', function( $email_id ) use ( $container ) {
$events_model = $container->get( Connector_Service_Provider::EVENT_MODEL );
$event = $events_model->get( $email_id );
// Couldn't find event, bail.
if ( empty( $event ) ) {
return;
}
$service = $event['service'];
// Not an oAuth service, bail.
if ( ! in_array( $service, array( 'google', 'microsoft', 'zoho' ) ) ) {
return;
}
$configured_key = sprintf( 'gsmtp_connector_configured_%s', $service );
delete_transient( $configured_key );
} );
}
}
@@ -0,0 +1,224 @@
<?php
namespace Gravity_Forms\Gravity_SMTP\Handler;
use Gravity_Forms\Gravity_SMTP\Apps\Config\Email_Log_Config;
use Gravity_Forms\Gravity_SMTP\Connectors\Connector_Factory;
use Gravity_Forms\Gravity_SMTP\Connectors\Endpoints\Save_Connector_Settings_Endpoint;
use Gravity_Forms\Gravity_SMTP\Connectors\Endpoints\Save_Plugin_Settings_Endpoint;
use Gravity_Forms\Gravity_SMTP\Data_Store\Data_Store_Router;
use Gravity_Forms\Gravity_SMTP\Feature_Flags\Feature_Flag_Manager;
use Gravity_Forms\Gravity_SMTP\Models\Suppressed_Emails_Model;
use Gravity_Forms\Gravity_SMTP\Utils\Source_Parser;
class Mail_Handler {
private static $configuration_status;
/**
* @var Connector_Factory $connector_factory
*/
private $connector_factory;
/**
* @var Data_Store_Router
*/
private $data_store;
/**
* @var Source_Parser
*/
private $source_parser;
/**
* @var Suppressed_Emails_Model
*/
private $suppressed_model;
/**
* @var null A way to store the entry ID being acted upon.
*/
protected $entry_id = null;
public function __construct( $connector_factory, $data_store, $source_parser, $suppressed_model ) {
$this->connector_factory = $connector_factory;
$this->data_store = $data_store;
$this->source_parser = $source_parser;
$this->suppressed_model = $suppressed_model;
}
public function set_entry_id( $entry_id ) {
$this->entry_id = $entry_id;
}
public function get_entry_id() {
return $this->entry_id;
}
private function get_connector( $type ) {
return $this->connector_factory->create( $type );
}
public static function is_minimally_configured() {
if ( ! is_null( self::$configuration_status ) ) {
return self::$configuration_status;
}
if ( defined( 'GRAVITYSMTP_INTEGRATION_PRIMARY' ) ) {
self::$configuration_status = true;
return true;
}
$connectors = self::get_connectors_from_options( Save_Connector_Settings_Endpoint::SETTING_PRIMARY_CONNECTOR );
$configured = ! empty( array_filter( $connectors, function( $enabled ) {
return ! empty( $enabled ) && $enabled !== false && $enabled !== 'false';
} ) );
if ( $configured ) {
self::$configuration_status = true;
return true;
}
$connectors = self::get_connectors_from_options( Save_Connector_Settings_Endpoint::SETTING_BACKUP_CONNECTOR );
$configured = ! empty( array_filter( $connectors, function( $enabled ) {
return ! empty( $enabled ) && $enabled !== false && $enabled !== 'false';
} ) );
self::$configuration_status = $configured;
return $configured;
}
public static function get_connectors_from_options( $type ) {
$opts_name = 'gravitysmtp_config';
$opts = get_option( $opts_name, '{}' );
$opts = json_decode( $opts, true );
return isset( $opts[ $type ] ) ? $opts[ $type ] : array();
}
public static function is_test_mode() {
$opts_name = 'gravitysmtp_config';
$opts = get_option( $opts_name, '{}' );
$opts = json_decode( $opts, true );
$test_mode = isset( $opts[ Save_Plugin_Settings_Endpoint::PARAM_TEST_MODE ] ) ? $opts[ Save_Plugin_Settings_Endpoint::PARAM_TEST_MODE ] : null;
return ! empty( $test_mode ) ? $test_mode !== 'false' : false;
}
public function mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
// Clear sources cache to ensure up-to-date info
delete_transient( Email_Log_Config::SOURCE_LIST_ITEMS_TRANSIENT );
// Re-send attempts put the source in the $headers array.
if ( is_array( $headers ) && isset( $headers['source'] ) ) {
$source = $headers['source'];
} else {
$debug = debug_backtrace();
$source = $this->source_parser->get_source_from_trace( $debug );
}
if ( ! empty( $attachments ) && ! is_array( $attachments ) ) {
$attachments = array( $attachments );
}
/**
* Filters the wp_mail() arguments.
*
* @since 2.2.0
*
* @param array $args A compacted array of wp_mail() arguments, including the "to" email,
* subject, message, headers, and attachments values.
*/
$atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'message', 'headers', 'attachments' ) );
/**
* Filters whether to preempt sending an email.
*
* Returning a non-null value will short-circuit wp_mail(), returning
* that value instead. A boolean return value should be used to indicate whether
* the email was successfully sent.
*
* @since 1.9.5
*
* @param null|bool $return Short-circuit return value.
* @param array $atts {
* Array of the `wp_mail()` arguments.
*
* @type string|string[] $to Array or comma-separated list of email addresses to send message.
* @type string $subject Email subject.
* @type string $message Message contents.
* @type string|string[] $headers Additional headers.
* @type string|string[] $attachments Paths to files to attach.
* }
*/
$pre_wp_mail = apply_filters( 'pre_wp_mail', null, $atts );
if ( null !== $pre_wp_mail ) {
return $pre_wp_mail;
}
/**
* Allows external code to modify which connector type is used for sending this email.
*
* Used primarily by the Backup Connection and Conditional Routing mechanisms.
*
* @since 1.2
*
* @param $current_type The current type being returned.
* @param $email_data An array of all the email data being used for this call.
*
* @return string $type The connector type to use for sending.
*/
$type = apply_filters( 'gravitysmtp_connector_for_sending', false, array( 'to' => $to, 'subject' => $subject, 'message' => $message, 'headers' => $headers, 'attachments' => $attachments ) );
$skip_retry = false;
if ( is_array( $type ) && isset( $type['force'] ) ) {
$skip_retry = true;
$type = $type['connector'];
}
// Either no connector is defined, or the router has determined that this email shouldn't send.
if ( $type === false ) {
do_action( 'gravitysmtp_on_send_failure', 0 );
return false;
}
$connector = $this->get_connector( $type );
$connector->init( $to, $subject, $message, $headers, $attachments, $source );
$to_email = $connector->get_att( 'to' )->first()->email();
if ( Feature_Flag_Manager::is_enabled( 'email_suppression' ) && $this->suppressed_model->is_email_suppressed( $to_email ) ) {
$connector->handle_suppressed_email( $to_email, $source );
return false;
}
$send = $connector->send();
if ( $send === true ) {
return true;
}
if ( $send !== true && $skip_retry ) {
$failed_email_id = $send;
do_action( 'gravitysmtp_on_send_failure', $failed_email_id );
return false;
}
/**
* Allows third-parties to act when the primary connector has failed.
*
* @param int The ID of the failed email send.
*
* @since 2.2
*
* @return void
*/
do_action( 'gravitysmtp_on_primary_failure', $send );
return $this->mail( $to, $subject, $message, $headers, $attachments );
}
}
@@ -0,0 +1,38 @@
<?php
namespace Gravity_Forms\Gravity_SMTP\Handler\Config;
use Gravity_Forms\Gravity_SMTP\Handler\Endpoints\Resend_Email_Endpoint;
use Gravity_Forms\Gravity_Tools\Config;
class Handler_Endpoints_Config extends Config {
protected $script_to_localize = 'gravitysmtp_scripts_admin';
protected $name = 'gravitysmtp_admin_config';
public function should_enqueue() {
return is_admin();
}
public function data() {
return array(
'components' => array(
'activity_log' => array(
'endpoints' => array(
Resend_Email_Endpoint::ACTION_NAME => array(
'action' => array(
'value' => Resend_Email_Endpoint::ACTION_NAME,
'default' => 'mock_endpoint',
),
'nonce' => array(
'value' => wp_create_nonce( Resend_Email_Endpoint::ACTION_NAME ),
'default' => 'nonce',
),
),
),
),
),
);
}
}
@@ -0,0 +1,175 @@
<?php
namespace Gravity_Forms\Gravity_SMTP\Handler\Endpoints;
use Gravity_Forms\Gravity_SMTP\Logging\Debug\Debug_Logger;
use Gravity_Forms\Gravity_SMTP\Models\Event_Model;
use Gravity_Forms\Gravity_SMTP\Users\Roles;
use Gravity_Forms\Gravity_SMTP\Utils\Attachments_Saver;
use Gravity_Forms\Gravity_SMTP\Utils\Recipient;
use Gravity_Forms\Gravity_SMTP\Utils\Recipient_Collection;
use Gravity_Forms\Gravity_SMTP\Utils\Recipient_Parser;
use Gravity_Forms\Gravity_Tools\Endpoints\Endpoint;
class Resend_Email_Endpoint extends Endpoint {
const ACTION_NAME = 'gravitysmtp_resend_email';
const PARAM_EMAIL_ID = 'email_id';
// Optional override for recipient address(es) when resending
const PARAM_RECIPIENT = 'recipient';
const PARAM_CC = 'cc';
const PARAM_BCC = 'bcc';
protected $minimum_cap = Roles::EDIT_EMAIL_LOG_DETAILS;
/**
* @var Event_Model
*/
protected $events;
/**
* @var Debug_Logger
*/
protected $logger;
/**
* @var Attachments_Saver
*/
protected $attachments_handler;
protected $required_params = array(
self::PARAM_EMAIL_ID,
);
public function __construct( $event_model, $logger, $attachments_handler ) {
$this->events = $event_model;
$this->logger = $logger;
$this->attachments_handler = $attachments_handler;
}
protected function get_nonce_name() {
return self::ACTION_NAME;
}
public function handle() {
if ( ! $this->validate() ) {
wp_send_json_error( __( 'Missing required parameters.', 'gravitysmtp' ), 400 );
}
$email_id = filter_input( INPUT_POST, self::PARAM_EMAIL_ID, FILTER_SANITIZE_NUMBER_INT );
$email = $this->events->get( $email_id );
$extra = unserialize( $email['extra'], array( 'allowed_classes' => array( Recipient_Collection::class, Recipient::class ) ) );
if ( ! is_array( $extra ) ) {
wp_send_json_error( __( 'Could not read stored email data.', 'gravitysmtp' ) );
}
$headers = $extra['headers'];
// Use SANITIZE_FULL_SPECIAL_CHARS for cc/bcc (plain email lists).
// Use sanitize_text_field for recipient since it may contain mailbox format Name <email>.
$cc_raw = filter_input( INPUT_POST, self::PARAM_CC, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
$bcc_raw = filter_input( INPUT_POST, self::PARAM_BCC, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
$recipient_raw = isset( $_POST[ self::PARAM_RECIPIENT ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::PARAM_RECIPIENT ] ) ) : null;
$to = $extra['to']; // default to original recipient
if ( null !== $recipient_raw ) {
$collection = $this->parse_recipients( $recipient_raw );
if ( is_wp_error( $collection ) ) {
wp_send_json_error( $collection->get_error_message() );
}
$to = $collection->as_string();
}
// Sanity check to ensure we don't try resending an un-sendable email.
if ( ! $email['can_resend'] ) {
// @translators: %s represents the email ID as a numeric string.
$error_message = __( 'Attempted to resend email %s, but could not due to either the message body not being stored, or attachments not being saved.', 'gravitysmtp' );
$this->logger->log_warning( sprintf( $error_message, $email_id ) );
wp_send_json_error( __( 'Email could not be resent as it was not stored with all required values.', 'gravitysmtp' ) );
}
if ( ! empty( $extra['attachments'] ) ) {
foreach( $extra['attachments'] as $key => $og_path ) {
$new_path = $this->attachments_handler->get_saved_attachment( $email_id, $og_path );
$extra['attachments'][ $key ] = $new_path;
}
}
$cc_str = null !== $cc_raw ? trim( $cc_raw ) : '';
if ( $cc_str !== '' ) {
$cc_collection = $this->parse_recipients( $cc_str );
if ( is_wp_error( $cc_collection ) ) {
wp_send_json_error( $cc_collection->get_error_message() );
}
$headers['cc'] = $cc_collection->as_string();
} else {
unset( $headers['cc'] );
}
$bcc_str = null !== $bcc_raw ? trim( $bcc_raw ) : '';
if ( $bcc_str !== '' ) {
$bcc_collection = $this->parse_recipients( $bcc_str );
if ( is_wp_error( $bcc_collection ) ) {
wp_send_json_error( $bcc_collection->get_error_message() );
}
$headers['bcc'] = $bcc_collection->as_string();
} else {
unset( $headers['bcc'] );
}
$headers['source'] = $extra['source'];
foreach ( $headers as $idx => $header ) {
if ( is_a( $header, Recipient_Collection::class ) ) {
$emails = $header->recipients();
$emails_array = array();
foreach ( $emails as $email_recipient ) {
$emails_array[] = $email_recipient->email();
}
$headers[ $idx ] = implode( ', ', $emails_array );
}
}
$success = wp_mail( $to, $email['subject'], $email['message'], $headers, $extra['attachments'] );
if ( ! $success ) {
wp_send_json_error( __( 'Email could not be resent; check your logs for more details.', 'gravitysmtp' ) );
}
wp_send_json_success( $success );
}
/**
* Parse a raw email string into a validated Recipient_Collection.
*
* @since 2.2.0
*
* @param string $raw_value The raw comma-separated email string.
*
* @return Recipient_Collection|\WP_Error Collection of recipients, or WP_Error if empty/invalid.
*/
private function parse_recipients( $raw_value ) {
$parser = new Recipient_Parser();
$collection = $parser->parse( $raw_value );
if ( $collection->count() === 0 ) {
return new \WP_Error( 'empty_email_list', __( 'Email address list is empty.', 'gravitysmtp' ) );
}
// Validate each parsed email address.
foreach ( $collection->recipients() as $recipient ) {
if ( ! is_email( $recipient->email() ) ) {
return new \WP_Error(
'invalid_email',
// translators: %s is the invalid email address.
sprintf( __( 'Invalid email address: %s', 'gravitysmtp' ), $recipient->email() )
);
}
}
return $collection;
}
}
@@ -0,0 +1,35 @@
<?php
namespace Gravity_Forms\Gravity_SMTP\Handler\External;
class Gravity_Forms_Note_Handler {
public function store_id( $id, $mail_handler ) {
$mail_handler->set_entry_id( $id );
}
public function get_modified_entry_note( $og_text, $entry_id, $mail_handler, $model ) {
$stored_entry_id = $mail_handler->get_entry_id();
if ( (int) $stored_entry_id !== (int) $entry_id ) {
return $og_text;
}
$email_id = $model->get_latest_id();
if ( empty( $email_id ) ) {
return $og_text;
}
$opening_tag = sprintf( '<a href="%s" target="_blank" style="flex: 0 0 100%%">', admin_url( 'admin.php?page=gravitysmtp-activity-log&tab=log-details&event_id=' . $email_id ) );
// @translators - the first %s represents an opening <a> tag, while the second represents a closing </a> tag.
$smtp_note = __( ' Email sent via Gravity SMTP. %sView Email%s', 'gravitysmtp' );
$smtp_note = sprintf( $smtp_note, $opening_tag, '</a>' );
$new_text = sprintf( '<div><div>%s</div><div>%s</div></div>', $og_text, $smtp_note );
return $new_text;
}
}