'send', ]; $map = [ 'to' => 'ao_email_to', 'cc' => 'ao_email_cc', 'bcc' => 'ao_email_bcc', 'subject' => 'ao_email_subject', 'context_type' => 'ao_email_context_type', 'context_id' => 'ao_email_context_id', ]; foreach ( $map as $arg_key => $query_key ) { if ( isset( $args[ $arg_key ] ) && '' !== $args[ $arg_key ] ) { $query_args[ $query_key ] = $args[ $arg_key ]; } } // Base64'd rather than passed as a plain query arg: esc_url() (run // on this URL wherever it's eventually output) strips encoded line // breaks and HTML delimiters from href values, which would mangle // a multi-line or HTML composer body. if ( isset( $args['message'] ) && '' !== $args['message'] ) { $query_args['ao_email_message_b64'] = rtrim( strtr( base64_encode( $args['message'] ), '+/', '-_' ), '=' ); } if ( ! empty( $args['html'] ) ) { $query_args['ao_email_html'] = '1'; } /** * Optional SMTP profile ID to prefill the composer with. Free has * no notion of SMTP profiles; this is a plain opaque string as far * as this function is concerned — Pro's settings UI is what * actually validates and offers it as a selectable option. */ if ( isset( $args['profile_id'] ) && '' !== $args['profile_id'] ) { $query_args['ao_email_profile'] = $args['profile_id']; } // Kept as a trailing raw query arg, appended after get_module_admin_url() // rather than through $map, so a nested return_url's own query // string can never leak in and override an outer arg (e.g. `page`). $url = \Yipresser\AdminOptimizer\Modules\SMTP_Email::get_module_admin_url( $query_args ); if ( ! empty( $args['return_url'] ) ) { $url .= '&ao_email_return_url=' . rawurlencode( $args['return_url'] ); } return $url; } } if ( ! function_exists( 'adminoptim_email_compose_link' ) ) { /** * Build a safe admin button/link for the Email & SMTP composer. * * @param array $args Link and composer arguments. * * @return string */ function adminoptim_email_compose_link( array $args = [] ): string { $label = isset( $args['label'] ) ? sanitize_text_field( adminoptim_email_string_value( $args['label'] ) ) : __( 'Email', 'admin-optimizer' ); $class = isset( $args['class'] ) ? sanitize_text_field( adminoptim_email_string_value( $args['class'] ) ) : 'button'; $target = isset( $args['target'] ) ? sanitize_key( adminoptim_email_string_value( $args['target'] ) ) : ''; $url = adminoptim_email_compose_url( $args ); $attrs = [ 'href="' . esc_url( $url ) . '"', 'class="' . esc_attr( $class ) . '"', ]; if ( '' !== $target ) { $attrs[] = 'target="' . esc_attr( $target ) . '"'; if ( '_blank' === $target ) { $attrs[] = 'rel="noopener noreferrer"'; } } return '' . esc_html( $label ) . ''; } } if ( ! function_exists( 'adminoptim_email_send' ) ) { /** * Send email through the Admin Optimizer Email & SMTP module. * * @param array $args Send arguments. * * @return bool|\WP_Error */ function adminoptim_email_send( array $args = [] ): bool|\WP_Error { $args = adminoptim_email_sanitize_send_args( apply_filters( 'adminoptim_email_send_args', $args ) ); $capability = apply_filters( 'adminoptim_email_compose_capability', 'manage_options', $args ); $capability = is_string( $capability ) && '' !== $capability ? $capability : 'manage_options'; if ( ! current_user_can( $capability ) ) { return new \WP_Error( 'adminoptim_email_unauthorized', __( 'Unauthorized action.', 'admin-optimizer' ) ); } if ( empty( $args['to'] ) ) { return new \WP_Error( 'adminoptim_email_invalid_to', __( 'Please enter a valid To email address.', 'admin-optimizer' ) ); } if ( '' === $args['subject'] ) { return new \WP_Error( 'adminoptim_email_missing_subject', __( 'Please enter an email subject.', 'admin-optimizer' ) ); } if ( '' === trim( wp_strip_all_tags( (string) $args['message'] ) ) ) { return new \WP_Error( 'adminoptim_email_missing_message', __( 'Please enter an email message.', 'admin-optimizer' ) ); } $headers = adminoptim_email_build_headers( $args ); $headers = apply_filters( 'adminoptim_email_send_headers', $headers, $args ); $headers = adminoptim_email_sanitize_headers( $headers ); do_action( 'adminoptim_email_before_send', $args ); $send = static function () use ( $args, $headers ) { return wp_mail( $args['to'], $args['subject'], $args['message'], $headers, $args['attachments'] ); }; // Free has no notion of SMTP profiles or routing; these are no-op // passthroughs unless Pro hooks the push/pop actions they call. if ( '' !== $args['profile_id'] ) { $send = static function () use ( $args, $send ) { return adminoptim_smtp_with_profile( $args['profile_id'], $send ); }; } if ( '' !== $args['route'] ) { $send = static function () use ( $args, $send ) { return adminoptim_smtp_with_route( $args['route'], $send ); }; } $sent = (bool) $send(); $result = $sent ? true : new \WP_Error( 'adminoptim_email_delivery_failed', __( 'Email delivery failed.', 'admin-optimizer' ) ); $result = apply_filters( 'adminoptim_email_send_result', $result, $args ); do_action( 'adminoptim_email_after_send', $args, $result ); if ( is_wp_error( $result ) ) { return $result; } return true === $result ? true : new \WP_Error( 'adminoptim_email_delivery_failed', __( 'Email delivery failed.', 'admin-optimizer' ) ); } } if ( ! function_exists( 'adminoptim_email_sanitize_compose_args' ) ) { /** * Sanitize composer arguments. * * @param mixed $args Raw args. * * @return array */ function adminoptim_email_sanitize_compose_args( $args ): array { $args = is_array( $args ) ? $args : []; $html = ! empty( $args['html'] ); $clean = [ 'to' => adminoptim_email_sanitize_email_list_text( $args['to'] ?? '' ), 'cc' => adminoptim_email_sanitize_email_list_text( $args['cc'] ?? '' ), 'bcc' => adminoptim_email_sanitize_email_list_text( $args['bcc'] ?? '' ), 'subject' => isset( $args['subject'] ) ? sanitize_text_field( adminoptim_email_string_value( $args['subject'] ) ) : '', 'message' => isset( $args['message'] ) ? ( $html ? wp_kses_post( adminoptim_email_string_value( $args['message'] ) ) : sanitize_textarea_field( adminoptim_email_string_value( $args['message'] ) ) ) : '', 'html' => $html, 'context_type' => isset( $args['context_type'] ) ? sanitize_key( adminoptim_email_string_value( $args['context_type'] ) ) : '', 'context_id' => isset( $args['context_id'] ) ? sanitize_text_field( adminoptim_email_string_value( $args['context_id'] ) ) : '', 'return_url' => '', 'profile_id' => isset( $args['profile_id'] ) ? sanitize_key( adminoptim_email_string_value( $args['profile_id'] ) ) : '', ]; if ( isset( $args['return_url'] ) ) { $return_url = esc_url_raw( adminoptim_email_string_value( $args['return_url'] ) ); $clean['return_url'] = '' !== $return_url ? wp_validate_redirect( $return_url, '' ) : ''; } return $clean; } } if ( ! function_exists( 'adminoptim_email_sanitize_send_args' ) ) { /** * Sanitize direct-send arguments. * * @param mixed $args Raw args. * * @return array */ function adminoptim_email_sanitize_send_args( $args ): array { $args = is_array( $args ) ? $args : []; $html = ! empty( $args['html'] ) || ! empty( $args['send_as_html'] ); return [ 'to' => adminoptim_email_sanitize_email_list( $args['to'] ?? '' ), 'cc' => adminoptim_email_sanitize_email_list( $args['cc'] ?? '' ), 'bcc' => adminoptim_email_sanitize_email_list( $args['bcc'] ?? '' ), 'subject' => isset( $args['subject'] ) ? sanitize_text_field( adminoptim_email_string_value( $args['subject'] ) ) : '', 'message' => isset( $args['message'] ) ? ( $html ? wp_kses_post( adminoptim_email_string_value( $args['message'] ) ) : wp_strip_all_tags( adminoptim_email_string_value( $args['message'] ) ) ) : '', 'html' => $html, 'attachments' => adminoptim_email_sanitize_attachments( $args['attachments'] ?? [] ), 'headers' => adminoptim_email_sanitize_headers( $args['headers'] ?? [] ), 'context_type' => isset( $args['context_type'] ) ? sanitize_key( adminoptim_email_string_value( $args['context_type'] ) ) : '', 'context_id' => isset( $args['context_id'] ) ? sanitize_text_field( adminoptim_email_string_value( $args['context_id'] ) ) : '', // Only resolved to a real profile ID (falling back to 'primary') // when the caller explicitly asked to route this send — an // absent key must stay '' so adminoptim_email_send() below // knows not to touch routing for this send at all. 'profile_id' => array_key_exists( 'profile_id', $args ) ? adminoptim_email_sanitize_profile_id( $args['profile_id'] ) : '', 'route' => isset( $args['route'] ) ? adminoptim_email_sanitize_route_key( $args['route'] ) : '', ]; } } if ( ! function_exists( 'adminoptim_email_sanitize_email_list' ) ) { /** * Sanitize a recipient list. * * @param mixed $value Raw value. * * @return array */ function adminoptim_email_sanitize_email_list( $value ): array { if ( is_array( $value ) ) { $value = implode( ',', array_map( 'adminoptim_email_string_value', $value ) ); } $emails = preg_split( '/[\s,;]+/', (string) $value ); $clean = []; if ( ! is_array( $emails ) ) { return []; } foreach ( $emails as $email ) { $email = sanitize_email( $email ); if ( is_email( $email ) ) { $clean[] = $email; } } return array_values( array_unique( $clean ) ); } } if ( ! function_exists( 'adminoptim_email_sanitize_email_list_text' ) ) { /** * Sanitize a recipient list for form/query display. * * @param mixed $value Raw value. * * @return string */ function adminoptim_email_sanitize_email_list_text( $value ): string { return implode( ', ', adminoptim_email_sanitize_email_list( $value ) ); } } if ( ! function_exists( 'adminoptim_email_build_headers' ) ) { /** * Build headers for Cc, Bcc, HTML, and custom headers. * * @param array $args Sanitized args. * * @return array */ function adminoptim_email_build_headers( array $args ): array { $headers = is_array( $args['headers'] ?? [] ) ? $args['headers'] : []; foreach ( (array) ( $args['cc'] ?? [] ) as $email ) { $headers[] = 'Cc: ' . $email; } foreach ( (array) ( $args['bcc'] ?? [] ) as $email ) { $headers[] = 'Bcc: ' . $email; } if ( ! empty( $args['html'] ) ) { $headers[] = 'Content-Type: text/html; charset=' . get_bloginfo( 'charset' ); } return $headers; } } if ( ! function_exists( 'adminoptim_email_sanitize_headers' ) ) { /** * Sanitize mail headers. * * @param mixed $headers Raw headers. * * @return array */ function adminoptim_email_sanitize_headers( $headers ): array { if ( is_string( $headers ) ) { $headers = preg_split( '/\r\n|\r|\n/', $headers ); } $headers = is_array( $headers ) ? $headers : []; $clean = []; foreach ( $headers as $header ) { $header = sanitize_text_field( adminoptim_email_string_value( $header ) ); if ( '' !== $header ) { $clean[] = $header; } } return $clean; } } if ( ! function_exists( 'adminoptim_email_sanitize_attachments' ) ) { /** * Sanitize attachment paths. * * @param mixed $attachments Raw attachments. * * @return array */ function adminoptim_email_sanitize_attachments( $attachments ): array { if ( is_string( $attachments ) ) { $attachments = [ $attachments ]; } $attachments = is_array( $attachments ) ? $attachments : []; $clean = []; foreach ( $attachments as $attachment ) { $attachment = sanitize_text_field( adminoptim_email_string_value( $attachment ) ); if ( '' !== $attachment ) { $clean[] = $attachment; } } return $clean; } } if ( ! function_exists( 'adminoptim_smtp_with_profile' ) ) { /** * Run a callback with a specific SMTP profile ID pinned for its * duration. * * Free has exactly one profile and no routing of its own; this is a * plain pass-through call to $callback() unless Pro hooks the two * actions below (which it does, onto SMTP_Router::push_profile_id() / * pop_profile_id()) to actually apply that profile's transport. * * @param string $profile_id Profile ID. * @param callable $callback Callback that sends email. * * @return mixed */ function adminoptim_smtp_with_profile( string $profile_id, callable $callback ) { do_action( 'adminoptim_smtp_push_profile', $profile_id ); try { return $callback(); } finally { do_action( 'adminoptim_smtp_pop_profile' ); } } } if ( ! function_exists( 'adminoptim_smtp_with_route' ) ) { /** * Run a callback while tagging outgoing mail with a route key (e.g. * `transactional`, `password_reset`) for its duration. * * Free has no routing rules of its own; this is a no-op pass-through * unless Pro hooks the two actions below to resolve the route to a * profile. * * @param string $route_key Route key. * @param callable $callback Callback that sends email. * * @return mixed */ function adminoptim_smtp_with_route( string $route_key, callable $callback ) { do_action( 'adminoptim_smtp_push_route', $route_key ); try { return $callback(); } finally { do_action( 'adminoptim_smtp_pop_route' ); } } } if ( ! function_exists( 'adminoptim_smtp_with_source' ) ) { /** * Run a callback while tagging outgoing mail with a source context * (e.g. `woocommerce`, `form_builder`) for its duration. * * Free never logs deliveries at all; this is a no-op pass-through * unless Pro hooks the two actions below onto its delivery logger. * * @param string $source Source key. * @param callable $callback Callback that sends email. * * @return mixed */ function adminoptim_smtp_with_source( string $source, callable $callback ) { do_action( 'adminoptim_smtp_push_source', $source ); try { return $callback(); } finally { do_action( 'adminoptim_smtp_pop_source' ); } } } if ( ! function_exists( 'adminoptim_email_sanitize_profile_id' ) ) { /** * Sanitize an SMTP profile ID. * * Free has no per-profile validation of its own; this is a generic * key sanitizer with a 'primary' fallback, matching the format Pro's * profiles use. * * @param mixed $profile_id Raw profile ID. * * @return string */ function adminoptim_email_sanitize_profile_id( $profile_id ): string { $profile_id = sanitize_key( str_replace( '-', '_', adminoptim_email_string_value( $profile_id ) ) ); return '' !== $profile_id ? $profile_id : 'primary'; } } if ( ! function_exists( 'adminoptim_email_sanitize_route_key' ) ) { /** * Sanitize an SMTP route key. * * @param mixed $route_key Raw route key. * * @return string */ function adminoptim_email_sanitize_route_key( $route_key ): string { return sanitize_key( str_replace( '-', '_', adminoptim_email_string_value( $route_key ) ) ); } } if ( ! function_exists( 'adminoptim_email_profile_exists' ) ) { /** * Check whether an SMTP profile ID exists and is enabled. * * Free only ever has the primary profile; this defers to Pro for * anything else via a filter, since Free has no way to inspect Pro's * saved profiles itself. * * @param string $profile_id Profile ID. * * @return bool */ function adminoptim_email_profile_exists( string $profile_id ): bool { $profile_id = adminoptim_email_sanitize_profile_id( $profile_id ); if ( 'primary' === $profile_id ) { return true; } /** * Filters whether a non-primary SMTP profile ID exists and is * enabled. Free has none; Pro hooks this to check its own saved * additional profiles. * * @param bool $exists False by default. * @param string $profile_id Sanitized profile ID. */ return (bool) apply_filters( 'adminoptim_smtp_profile_exists', false, $profile_id ); } } if ( ! function_exists( 'adminoptim_email_string_value' ) ) { /** * Convert scalar values to strings without array/object notices. * * @param mixed $value Raw value. * * @return string */ function adminoptim_email_string_value( $value ): string { if ( is_scalar( $value ) || null === $value ) { return (string) $value; } return ''; } }