__( 'Staff notification', 'killerconversions' ), 'confirmation' => __( 'Visitor confirmation', 'killerconversions' ) ) ); } public static function is_destination( $destination ) { return isset( self::destinations()[ $destination ] ); } public static function defaults() { return array( 'routes' => array(), 'reply_to' => '', 'confirmation' => array( 'enabled' => false, 'email' => '', 'consent' => '', 'subject' => __( 'We received your enquiry', 'killerconversions' ), 'body' => __( 'Thank you. Your enquiry has been saved. Our team will review it and follow up if needed.', 'killerconversions' ) ) ); } private static function error( $message ) { return new WP_Error( 'kconv_mail_settings', $message ); } public static function sanitize( $raw, $form ) { if ( ! is_array( $raw ) || ! isset( $raw['routes'], $raw['confirmation'] ) || ! is_array( $raw['routes'] ) || count( $raw['routes'] ) > 5 || ! is_array( $raw['confirmation'] ) ) { return self::error( __( 'Email settings are incomplete. Reload the editor before saving.', 'killerconversions' ) ); } $fields = array_column( $form['fields'], null, 'id' ); $clean = self::defaults(); if ( array_key_exists( 'reply_to', $raw ) ) { if ( ! is_string( $raw['reply_to'] ) || ( '' !== $raw['reply_to'] && ( ! isset( $fields[ $raw['reply_to'] ] ) || 'email' !== $fields[ $raw['reply_to'] ]['type'] ) ) ) { return self::error( __( 'Choose an email question for staff Reply-To, or leave it off.', 'killerconversions' ) ); } $clean['reply_to'] = $raw['reply_to']; } $routes = apply_filters( 'kconv_mail_routes', empty( $raw['routes'] ) ? array() : new WP_Error( 'kconv_mail_routes_unavailable', __( 'This saved routing configuration requires the matching Pro plugin. It is retained and notifications are paused.', 'killerconversions' ) ), $raw['routes'], $form ); if ( is_wp_error( $routes ) ) { return $routes; } $clean['routes'] = $routes; $receipt = $raw['confirmation']; if ( isset( $receipt['enabled'] ) && ! in_array( $receipt['enabled'], array( true, false, '1', '0', 1, 0 ), true ) ) { return self::error( __( 'Invalid confirmation setting.', 'killerconversions' ) ); } $clean['confirmation']['enabled'] = ! empty( $receipt['enabled'] ); foreach ( array( 'email', 'consent', 'subject', 'body' ) as $key ) { if ( ! isset( $receipt[ $key ] ) || ! is_string( $receipt[ $key ] ) ) { return self::error( __( 'Visitor confirmation settings are incomplete.', 'killerconversions' ) ); } $clean['confirmation'][ $key ] = $receipt[ $key ]; } if ( KCONV_Field_Rules::length( $receipt['subject'] ) > 160 || preg_match( '/[\x00-\x1f\x7f]/', $receipt['subject'] ) || KCONV_Field_Rules::length( $receipt['body'] ) > 2000 || preg_match( '/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/', $receipt['body'] ) || wp_strip_all_tags( $receipt['subject'] ) !== $receipt['subject'] || wp_strip_all_tags( $receipt['body'] ) !== $receipt['body'] ) { return self::error( __( 'Use plain confirmation text: a one-line subject up to 160 characters and a message up to 2,000 characters.', 'killerconversions' ) ); } if ( $clean['confirmation']['enabled'] && ( '' === trim( $receipt['subject'] ) || '' === trim( $receipt['body'] ) || ! isset( $fields[ $receipt['email'] ], $fields[ $receipt['consent'] ] ) || 'email' !== $fields[ $receipt['email'] ]['type'] || 'checkbox' !== $fields[ $receipt['consent'] ]['type'] || ! empty( $fields[ $receipt['consent'] ]['required'] ) ) ) { return self::error( __( 'For confirmations, choose an email question and a separate optional “Email me a confirmation” checkbox, then enter a subject and message.', 'killerconversions' ) ); } return $clean; } public static function settings( $form ) { $stored = empty( $form['id'] ) ? '' : get_post_meta( $form['id'], '_kconv_mail_settings', true ); return self::sanitize( '' === $stored ? self::defaults() : $stored, $form ); } public static function posted( $form, $post ) { if ( ! isset( $post['kconv_mail_present'] ) ) { return self::settings( $form ); } if ( '1' !== $post['kconv_mail_present'] ) { return self::error( __( 'Reload the editor before changing email settings.', 'killerconversions' ) ); } if ( ! current_user_can( 'manage_options' ) ) { return self::error( __( 'Reload the editor before changing email settings.', 'killerconversions' ) ); } if ( ! isset( $post['kconv_mail_nonce'] ) || ! is_string( $post['kconv_mail_nonce'] ) ) { return self::error( __( 'Reload the editor before changing email settings.', 'killerconversions' ) ); } if ( ! wp_verify_nonce( sanitize_text_field( $post['kconv_mail_nonce'] ), 'kconv_mail_save' ) ) { return self::error( __( 'Reload the editor before changing email settings.', 'killerconversions' ) ); } if ( ! isset( $post['kconv_mail_complete'], $post['kconv_mail'] ) || '1' !== $post['kconv_mail_complete'] || ! is_array( $post['kconv_mail'] ) ) { return self::error( __( 'Email settings were cut short. No changes were saved; reload and check the hosting input limit.', 'killerconversions' ) ); } $previous = get_post_meta( $form['id'], '_kconv_mail_settings', true ); if ( ! array_key_exists( 'reply_to', $post['kconv_mail'] ) && ! empty( $previous['reply_to'] ) ) { return self::error( __( 'This editor does not contain the saved Reply-To setting. Reload before saving.', 'killerconversions' ) ); } if ( ! isset( $post['kconv_mail']['routes'] ) ) { $post['kconv_mail']['routes'] = is_array( $previous ) && isset( $previous['routes'] ) ? $previous['routes'] : array(); } return self::sanitize( $post['kconv_mail'], $form ); } public static function validate_save( $errors, $form, $post ) { $settings = self::posted( $form, $post ); if ( is_wp_error( $settings ) ) { $errors->add( $settings->get_error_code(), $settings->get_error_message() ); } return $errors; } public static function initial_status( $status, $form ) { $config = self::config( $form ); return ! is_wp_error( $config ) && $config['enabled'] ? 'queued' : $status; } public static function inline( $inline, $entry_id, $form ) { if ( class_exists( 'KCONV_Delivery_Guard' ) && ! KCONV_Delivery_Guard::allowed() ) { return false; } $config = self::config( $form ); return ! is_wp_error( $config ) && $config['enabled'] ? false : $inline; } public static function config( $form, $destination = 'email' ) { $override = apply_filters( 'kconv_mail_config_override', null, $form, $destination ); if ( null !== $override ) { return $override; } if ( ! self::is_destination( $destination ) ) { return self::error( 'Unknown email destination.' ); } $settings = self::settings( $form ); if ( is_wp_error( $settings ) ) { return $settings; } if ( 'confirmation' === $destination ) { $config = $settings['confirmation']; $config['acknowledged'] = $config['enabled']; $config['kind'] = 'confirmation'; $config['fields'] = array( $config['email'] ); $config['revision'] = hash( 'sha256', wp_json_encode( array( $config, $form['fields'] ) ) ); if ( $config['enabled'] ) { $fields = array_column( $form['fields'], null, 'id' ); $choice = $fields[ $config['consent'] ]; $config['consent_digest'] = hash_hmac( 'sha256', wp_json_encode( array( $config['email'], $choice['id'], $choice['label'], $choice['help'], $choice['required'], $config['subject'], $config['body'] ) ), wp_salt( 'auth' ) ); } return $config; } $email = isset( $form['settings']['notify_email'] ) ? $form['settings']['notify_email'] : ''; $fields = array(); foreach ( $form['fields'] as $field ) { if ( 'file' !== $field['type'] ) { $fields[ $field['id'] ] = $field['label']; } } // Keep the old effective-config digest byte-identical when no routing is configured. $config = array( 'enabled' => (bool) $email, 'acknowledged' => (bool) $email, 'recipient' => $email, 'title' => $form['title'], 'fields' => array_keys( $fields ), 'labels' => $fields ); if ( $email && ( ! is_string( $email ) || ! is_email( $email ) || preg_match( '/[\r\n]/', $email ) ) ) { return new WP_Error( 'mail_configuration', __( 'The notification recipient is invalid.', 'killerconversions' ) ); } if ( $settings['routes'] ) { $config['routes'] = $settings['routes']; $config['enabled'] = true; $config['acknowledged'] = true; } // Omit unused additions so an upgrade alone never revises an existing job. if ( '' !== $settings['reply_to'] ) { $config['reply_to'] = $settings['reply_to']; } $config['revision'] = hash( 'sha256', wp_json_encode( $config ) ); return $config; } public static function format_value( $value ) { if ( KCONV_Repeaters::is_value( $value ) ) { return KCONV_Repeaters::text( $value ); } if ( is_array( $value ) ) { return implode( ', ', array_map( 'strval', array_filter( $value, 'is_scalar' ) ) ); } return is_scalar( $value ) ? (string) $value : ''; } public static function request( $config, $entry, $delivery_id ) { $values = json_decode( $entry['payload'], true ); if ( ! is_array( $values ) ) { return new WP_Error( 'invalid_payload', __( 'The email cannot be built from this entry.', 'killerconversions' ) ); } $document_kind = isset( $config['kind'] ) && 'confirmation' === $config['kind'] ? 'visitor' : 'staff'; if ( ! empty( $values['__kconv_document'][ $document_kind ] ) && ! class_exists( 'KCONV_PDF' ) ) { return new WP_Error( 'pro_required', __( 'This saved email requires its Pro document attachment. Activate the matching companion before retrying; the attachment will not be omitted.', 'killerconversions' ) ); } $reply_to = ''; if ( isset( $config['kind'] ) && 'confirmation' === $config['kind'] ) { if ( ! isset( $values[ $config['consent'] ] ) || '1' !== $values[ $config['consent'] ] ) { return new WP_Error( 'consent_missing', __( 'The visitor did not request a confirmation.', 'killerconversions' ) ); } $recipient = $values[ $config['email'] ] ?? ''; if ( ! is_string( $recipient ) || ! is_email( $recipient ) || sanitize_email( $recipient ) !== $recipient || preg_match( '/[\r\n]/', $recipient ) ) { return new WP_Error( 'email_missing', __( 'The confirmation address is missing or invalid.', 'killerconversions' ) ); } $subject = $config['subject']; $body = $config['body']; } else { if ( ! empty( $config['reply_to'] ) && isset( $values[ $config['reply_to'] ] ) && '' !== $values[ $config['reply_to'] ] ) { $reply_to = $values[ $config['reply_to'] ]; if ( ! is_string( $reply_to ) || strlen( $reply_to ) > 254 || ! is_email( $reply_to ) || sanitize_email( $reply_to ) !== $reply_to || preg_match( '/[\r\n]/', $reply_to ) ) { return new WP_Error( 'mail_reply_to', __( 'The mapped Reply-To address is invalid.', 'killerconversions' ) ); } } $recipient = $config['recipient']; $recipient = apply_filters( 'kconv_mail_recipient', $recipient, $config, $values, $entry ); if ( is_wp_error( $recipient ) ) { return $recipient; } if ( '' === $recipient ) { return new WP_Error( 'mail_no_recipient', __( 'No routing rule matched and no default recipient is configured.', 'killerconversions' ) ); } if ( ! is_email( $recipient ) || preg_match( '/[\r\n]/', $recipient ) ) { return new WP_Error( 'mail_configuration', __( 'The notification recipient is invalid.', 'killerconversions' ) ); } $lines = array( 'New entry #' . (int) $entry['id'], $config['title'], 'Delivery: ' . $delivery_id, '' ); foreach ( $config['fields'] as $field ) { if ( array_key_exists( $field, $values ) && ( KCONV_Integrations::transferable( $values[ $field ] ) || KCONV_Repeaters::is_value( $values[ $field ] ) ) ) { $lines[] = $config['labels'][ $field ] . ': ' . self::format_value( $values[ $field ] ); } } $subject = 'New form entry: ' . str_replace( array( "\r", "\n" ), ' ', $config['title'] ); $body = implode( "\n", $lines ); } if ( class_exists( 'KCONV_Saved_Estimates' ) && ! empty( $values['__kconv_estimate'] ) ) { $body .= "\n\n" . KCONV_Saved_Estimates::text( $values['__kconv_estimate'] ); } if ( strlen( $body ) > 131072 ) { return new WP_Error( 'invalid_payload', __( 'The email exceeds its size limit.', 'killerconversions' ) ); } $request = array( 'to' => $recipient, 'subject' => $subject, 'body' => $body, 'headers' => array( 'Content-Type: text/plain; charset=UTF-8', 'X-KillerConversions-Delivery: ' . $delivery_id ) ); if ( '' !== $reply_to ) { $request['reply_to'] = $reply_to; $request['headers'][] = 'Reply-To: ' . $reply_to; } return apply_filters( 'kconv_mail_request', $request, $config, $entry, $delivery_id ); } public static function send( $request ) { $attachments = apply_filters( 'kconv_mail_attachments', array(), $request ); if ( is_wp_error( $attachments ) ) { return array( 'status' => 'mail_failed', 'http_code' => 0 ); } try { $accepted = wp_mail( $request['to'], $request['subject'], $request['body'], $request['headers'], $attachments ); } finally { do_action( 'kconv_mail_attachments_cleanup', $attachments ); } return array( 'status' => true === $accepted ? 'mail_accepted' : 'mail_failed', 'http_code' => 0 ); } public static function status_changed( $job_id, $entry_id, $destination, $status ) { if ( 'email' !== $destination ) { return; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The latest email queue status is read directly from the fixed job table and copied to the private entry after a queue transition; the entry ID is prepared and authoring transaction errors propagate. $latest = $wpdb->get_var( $wpdb->prepare( "SELECT status FROM %i WHERE entry_id=%d AND destination='email' ORDER BY id DESC LIMIT 1", KCONV_Delivery::table(), $entry_id ) ); $failed = (bool) $wpdb->last_error; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The latest email queue status is read directly from the fixed job table and copied to the private entry after a queue transition; the entry ID is prepared and authoring transaction errors propagate. if ( $latest ) { $failed = false === $wpdb->update( KCONV_Core::table( 'entries' ), array( 'mail_status' => $latest ), array( 'id' => $entry_id ) ) || $failed; } if ( $failed && class_exists( 'KCONV_Authoring' ) && KCONV_Authoring::transaction_active() ) { do_action( 'kconv_authoring_storage_error', 'Notification status could not be saved.' ); } } public static function form_saved( $id, $form, $post ) { $settings = self::posted( $form, $post ); if ( is_wp_error( $settings ) ) { return; } if ( isset( $post['kconv_mail_present'] ) ) { update_post_meta( $id, '_kconv_mail_settings', wp_slash( $settings ) ); } foreach ( self::destinations() as $destination => $label ) { $config = self::config( $form, $destination ); KCONV_Delivery::cancel_changed( $id, $destination, ! is_wp_error( $config ) && $config['enabled'] ? $config['revision'] : '' ); } if ( class_exists( 'KCONV_Free_Ownership' ) ) { KCONV_Free_Ownership::refresh_form( $id ); } } public static function settings_note( $form ) { $settings = self::settings( $form ); if ( is_wp_error( $settings ) ) { echo '

' . esc_html( $settings->get_error_message() ) . '

'; $settings = self::defaults(); } wp_enqueue_style( 'kconv-mail-settings', KCONV_URL . 'assets/mail-settings.css', array( 'kconv-workspace' ), KCONV_VERSION ); wp_enqueue_script( 'kconv-mail-settings', KCONV_URL . 'assets/mail-settings.js', array( 'kconv-admin' , 'wp-i18n' ), KCONV_VERSION, true ); wp_set_script_translations( 'kconv-mail-settings', 'killerconversions', KCONV_DIR . 'languages' ); echo '

' . esc_html__( 'Send the right email', 'killerconversions' ) . '

' . esc_html__( 'Staff notifications use the address in Email notifications. Choose a Reply-To question or offer a separate visitor confirmation below. Conditional staff routing is available with Pro.', 'killerconversions' ) . '

'; wp_nonce_field( 'kconv_mail_save', 'kconv_mail_nonce', false ); self::select( 'kconv_mail[reply_to]', __( 'Reply to the visitor (optional email question)', 'killerconversions' ), 'email', $settings['reply_to'], $form ); echo '

' . esc_html__( 'Staff replies use this submitted email address when present. The website’s mail sender stays unchanged. This does not send an email to the visitor.', 'killerconversions' ) . '

'; do_action( 'kconv_mail_routes_editor', $settings, $form ); echo '
' . esc_html__( 'Offer a visitor confirmation', 'killerconversions' ) . '

' . esc_html__( 'Send a short receipt only when the visitor checks a separate optional confirmation box. The saved estimate breakdown is included when present. Other answers are excluded from the message body. An explicitly enabled Pro PDF attachment includes saved answers; uploaded file contents are excluded. This does not subscribe anyone to marketing.', 'killerconversions' ) . '

'; $c = $settings['confirmation']; echo '
'; self::select( 'kconv_mail[confirmation][email]', __( 'Visitor email question', 'killerconversions' ), 'email', $c['email'], $form ); self::select( 'kconv_mail[confirmation][consent]', __( 'Optional confirmation checkbox', 'killerconversions' ), 'consent', $c['consent'], $form ); echo '

' . esc_html__( 'Emails are queued with bounded retries. Changing settings cancels affected pending jobs. Save, publish your changes, submit a test enquiry, then inspect Delivery and the intended inbox. Use Review a new send to check the current destination before resending a retained entry. Mail acceptance does not prove inbox receipt.', 'killerconversions' ) . '

'; } public static function select( $name, $label, $kind, $value, $form ) { echo ''; } }