# double-opt-in/trunk/src/Integration/CF7Integration.php

Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification, version trunk. 1,112 lines.

- Page: https://pluginprobe.com/plugins/double-opt-in/trunk/code/src/Integration/CF7Integration.php
- Raw: https://pluginprobe.com/plugins/double-opt-in/trunk/raw/src/Integration/CF7Integration.php
- Modified: 2026-10-01T12:33:22+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/double-opt-in/trunk/code/src/Integration/CF7Integration.php#L10-L20`.

```php
<?php
/**
 * Contact Form 7 Integration
 *
 * @package Forge12\DoubleOptIn\Integration
 * @since   4.0.0
 */

namespace Forge12\DoubleOptIn\Integration;

use Forge12\DoubleOptIn\Container\Container;
use Forge12\DoubleOptIn\EmailTemplates\PlaceholderMapper;
use Forge12\DoubleOptIn\FollowUp\FollowUpAttempt;
use Forge12\DoubleOptIn\FollowUp\FollowUpCoordinator;
use Forge12\DoubleOptIn\FollowUp\FollowUpResult;
use Forge12\DoubleOptIn\Frontend\ErrorNotification;
use Forge12\DoubleOptIn\Frontend\SubmitNotice;
use Forge12\DoubleOptIn\Spam\SubmissionTrap;
use forge12\contactform7\CF7DoubleOptIn\Category;
use forge12\contactform7\CF7DoubleOptIn\CF7DoubleOptIn;
use forge12\contactform7\CF7DoubleOptIn\HTMLSelect;
use forge12\contactform7\CF7DoubleOptIn\OptIn;
use forge12\contactform7\CF7DoubleOptIn\SanitizeHelper;
use Forge12\Shared\LoggerInterface;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class CF7Integration
 *
 * Integration for Contact Form 7.
 * Handles opt-in creation, confirmation mail sending, and admin panel.
 */
class CF7Integration extends AbstractFormIntegration implements AdminPanelInterface {

	/**
	 * Current OptIn for mail attachment handling.
	 *
	 * @var OptIn|null
	 */
	private ?OptIn $currentOptIn = null;

	/**
	 * The confirmation mail sent in this request, for the feedback response
	 * (5.8.0): recipient, resolved sender and subject, and the outcome.
	 *
	 * @var array{email: string, sender: string, subject: string, sent: bool}|null
	 */
	private $lastOptInMail = null;

	/**
	 * The DOI submission of this request, keyed to its form (5.8.0).
	 *
	 * @var array{form_id: int, email: string, sender: string, subject: string, sent: bool}|null
	 */
	private $submitted = null;

	/**
	 * {@inheritdoc}
	 */
	public function getIdentifier(): string {
		return 'cf7';
	}

	/**
	 * {@inheritdoc}
	 */
	public function getName(): string {
		return __( 'Contact Form 7', 'double-opt-in' );
	}

	/**
	 * {@inheritdoc}
	 */
	public function isAvailable(): bool {
		return function_exists( 'wpcf7' ) || class_exists( '\WPCF7_ContactForm' );
	}

	/**
	 * {@inheritdoc}
	 */
	protected function getPostType(): string {
		return 'wpcf7_contact_form';
	}

	/**
	 * {@inheritdoc}
	 */
	public function getFormEditUrl( $formId ): string {
		return admin_url( 'admin.php?page=wpcf7&post=' . (int) $formId . '&action=edit' );
	}

	/**
	 * {@inheritdoc}
	 */
	public function registerHooks(): void {
		// Frontend hooks
		add_action( 'wpcf7_before_send_mail', array( $this, 'onSubmit' ), $this->getHookPriority(), 3 );
		add_action( 'init', array( $this, 'handleOptInConfirmation' ) );

		// Honest success message + "open your inbox" (5.8.0).
		add_filter( 'wpcf7_feedback_response', array( $this, 'filterFeedbackResponse' ), 10, 2 );
		add_action( 'wpcf7_enqueue_scripts', array( $this, 'enqueueSubmitNotice' ) );

		// Honeypot + minimum fill time, through CF7's own spam flow (5.8.0).
		add_filter( 'wpcf7_form_elements', array( $this, 'addSubmissionTrap' ) );
		add_filter( 'wpcf7_spam', array( $this, 'checkSubmissionTrap' ), 9, 2 );

		// Register recipient filter
		add_filter( 'f12_cf7_doubleoptin_get_recipient_cf7', array( $this, 'getRecipientFilter' ), 10, 3 );

		// Confirmation mail hooks
		add_action( 'f12_cf7_doubleoptin_before_send_default_mail', array( $this, 'beforeSendDefaultMail' ) );
		add_action( 'f12_cf7_doubleoptin_after_send_default_mail', array( $this, 'afterSendDefaultMail' ) );
		add_action( 'f12_cf7_doubleoptin_trigger_default_mail', array( $this, 'onTriggerDefaultMail' ) );

		// File hand-off + pending-cleanup. CF7 attaches files to the
		// confirmation mail in attachExtraAttachments (hooked on
		// wpcf7_before_send_mail during sendConfirmationMail). Cleanup
		// MUST run AFTER the mail is sent — otherwise we'd be deleting
		// files before they're attached. Hence `after_send_default_mail`,
		// not `after_confirm` like the other integrations. Priority 20
		// (default 10) so any third-party listener that introspects the
		// attachments still sees them.
		// File-lifecycle plan, Schritt 2c (2026-05-08).
		add_action( 'f12_cf7_doubleoptin_after_send_default_mail', array( $this, 'cleanupPendingAfterMail' ), 20, 1 );

		// Admin hooks
		$this->registerAdminHooks();

		$this->getLogger()->debug(
			'CF7 integration hooks registered',
			array(
				'plugin' => 'double-opt-in',
			)
		);
	}

	/**
	 * {@inheritdoc}
	 */
	public function registerAdminHooks(): void {
		add_action( 'admin_init', array( $this, 'setupAdminPanel' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueueAdminAssets' ) );
	}

	/**
	 * Setup admin panel hooks.
	 *
	 * @return void
	 */
	public function setupAdminPanel(): void {
		add_filter( 'wpcf7_editor_panels', array( $this, 'addEditorPanel' ), 10, 1 );
		add_action( 'wpcf7_save_contact_form', array( $this, 'saveFormSettings' ), 10, 3 );
	}

	/**
	 * {@inheritdoc}
	 */
	public function enqueueAdminAssets( string $hook ): void {
		wp_enqueue_script(
			'f12-cf7-doubleoptin-admin',
			plugins_url( 'compatibility/cf7/assets/f12-cf7-popup.js', F12_DOUBLEOPTIN_PLUGIN_FILE ),
			array( 'jquery' )
		);

		wp_localize_script(
			'f12-cf7-doubleoptin-admin',
			'doi',
			array(
				'ajax_url' => admin_url( 'admin-ajax.php' ),
				'nonce'    => wp_create_nonce( 'f12_doi_details' ),
			)
		);

		wp_enqueue_script(
			'f12-cf7-doubleoptin-templateloader',
			plugins_url( 'compatibility/cf7/assets/f12-cf7-templateloader.js', F12_DOUBLEOPTIN_PLUGIN_FILE ),
			array( 'jquery' )
		);

		wp_localize_script(
			'f12-cf7-doubleoptin-templateloader',
			'templateloader',
			array(
				'ajax_url'          => admin_url( 'admin-ajax.php' ),
				'nonce'             => wp_create_nonce( 'f12_doi_templateloader' ),
				'label_placeholder' => __( 'Please wait while we load the template...', 'double-opt-in' ),
			)
		);
	}

	/**
	 * {@inheritdoc}
	 */
	public function getHookPriority(): int {
		return 5;
	}

	/**
	 * {@inheritdoc}
	 */
	public function processSubmission( $context ): ?FormDataInterface {
		if ( ! is_array( $context ) || ! isset( $context['form'] ) || ! isset( $context['submission'] ) ) {
			return null;
		}

		$form       = $context['form'];
		$submission = $context['submission'];

		return FormData::fromCF7( $form, $submission );
	}

	/**
	 * {@inheritdoc}
	 */
	public function resolveRecipient( FormDataInterface $formData, array $formParameter ): string {
		if ( ! isset( $formParameter['recipient'] ) ) {
			return '';
		}

		$recipientField = str_replace( array( '[', ']' ), '', $formParameter['recipient'] );
		$fields         = $formData->getFields();

		if ( isset( $fields[ $recipientField ] ) ) {
			return sanitize_email( $fields[ $recipientField ] );
		}

		return '';
	}

	/**
	 * Recipient filter callback for legacy compatibility.
	 *
	 * @param string $recipient     Current recipient.
	 * @param array  $formParameter Form parameters.
	 * @param array  $postParameter Post data.
	 *
	 * @return string The resolved recipient.
	 */
	public function getRecipientFilter( string $recipient, array $formParameter, array $postParameter ): string {
		if ( ! isset( $formParameter['recipient'] ) ) {
			return $recipient;
		}

		$recipientField = str_replace( array( '[', ']' ), '', $formParameter['recipient'] );

		if ( isset( $postParameter[ $recipientField ] ) ) {
			return sanitize_email( $postParameter[ $recipientField ] );
		}

		return $recipient;
	}

	/**
	 * Handle form submission.
	 *
	 * @param \WPCF7_ContactForm $form       The contact form.
	 * @param bool               $abort      Whether to abort submission.
	 * @param \WPCF7_Submission  $submission The submission.
	 *
	 * @return void
	 */
	public function onSubmit( $form, &$abort, $submission ): void {
		$formId = $form->id();

		$this->getLogger()->debug(
			'CF7 form submission received',
			array(
				'plugin'  => 'double-opt-in',
				'form_id' => $formId,
			)
		);

		if ( ! $this->isOptInEnabled( $formId ) ) {
			// Our own post-confirmation replay: attach the stored files.
			if ( self::isReplaying() ) {
				$this->attachStoredFiles( $submission );
			}
			return;
		}

		// Remove CF7 DB integration
		remove_action( 'wpcf7_before_send_mail', 'cfdb7_before_send_mail' );

		// Create form data
		$formData      = FormData::fromCF7( $form, $submission );
		$formParameter = $this->getFormParameter( $formId );

		// Check skip filter
		if ( apply_filters( 'f12_cf7_doubleoptin_skip_option', false, $formId, $formData->getFields(), 'cf7' ) ) {
			$this->getLogger()->info(
				'OptIn skipped by filter',
				array(
					'plugin'  => 'double-opt-in',
					'form_id' => $formId,
				)
			);
			return;
		}

		// Set recipient
		$recipient = $this->resolveRecipient( $formData, $formParameter );
		$formData  = $formData->withRecipientEmail( $recipient );

		// Create OptIn
		$optIn = $this->createOptIn( $formData, $formParameter );

		if ( ! $optIn ) {
			// Always prevent the original CF7 mail from being sent when opt-in creation fails
			add_filter( 'wpcf7_skip_mail', '__return_true' );

			$error = self::getLastError();
			// Abort with the reason instead of CF7's "sent": CF7 then keeps the
			// visitor's input and shows the message in the form. A refused
			// consent always takes this path (OptInError::isAlwaysShown()).
			if ( $error && $error->shouldShowToVisitor( (int) $formId ) ) {
				$message = apply_filters( 'f12_cf7_doubleoptin_error_message', $error->getMessage(), $error, $formId );
				if ( method_exists( $submission, 'set_response' ) ) {
					$submission->set_response( $message );
				}
				$abort = true;
				// The form shows it now — no second copy in the toast.
				ErrorNotification::forget();
			}
			return;
		}

		// Send opt-in mail
		$this->lastOptInMail = null;
		$this->sendOptInMail( $optIn, $formData, $formParameter );
		if ( $this->lastOptInMail !== null ) {
			$this->submitted = array( 'form_id' => (int) $formId ) + $this->lastOptInMail;
		}

		// Skip original mail
		add_filter( 'wpcf7_skip_mail', '__return_true' );
		do_action( 'f12_cf7_doubleoptin_sent', $form, $formId );
	}

	/**
	 * {@inheritdoc}
	 */
	public function sendOptInMail( OptIn $optIn, FormDataInterface $formData, array $formParameter ): bool {
		$formParameter['formUrl'] = $formData->getMetaValue( 'source_url', '' );

		// Get template body
		$body = apply_filters(
			'f12_cf7_doubleoptin_template_body',
			$formParameter['body'],
			$formParameter['template'] ?? 'blank',
			$formParameter,
			$optIn
		);

		// Process placeholders
		$body = $this->prepareMailBody( $body, $optIn, $formParameter );
		$body = apply_filters( 'f12_cf7_doubleoptin_body', $body );

		// Store mail content in OptIn
		$optIn->set_mail_optin( $body );
		$optIn->save();

		// Prepare mail arguments
		$args = apply_filters(
			'f12-cf7-doubleoptin-cf7-args',
			array(
				'subject'            => $formParameter['subject'] ?? '',
				'body'               => $body,
				'sender'             => $formParameter['sender'] ?? '',
				'sender_name'        => $formParameter['sender_name'] ?? '',
				'recipient'          => $optIn->get_email(),
				'use_html'           => true,
				'additional_headers' => '',
			)
		);

		if ( ! empty( $args['sender_name'] ) ) {
			$args['additional_headers'] .= 'From: ' . $args['sender_name'] . ' <' . $args['sender'] . '>';
		}

		// Send via CF7 mail system. Up to 5.7 the result was dropped and a
		// failed send looked exactly like a successful one.
		$sent = (bool) \WPCF7_Mail::send( $args, 'mail' );

		// Record the outcome on the opt-in (OptInMailTracker). No address in
		// the log: the id identifies the record.
		do_action( 'f12_doi_optin_mail_result', (int) $optIn->get_id(), $sent, '' );

		// What the visitor is told to look for — with CF7's mail tags resolved,
		// as WPCF7_Mail::send() did for the mail itself.
		$sender  = (string) $args['sender'];
		$subject = (string) $args['subject'];
		if ( function_exists( 'wpcf7_mail_replace_tags' ) ) {
			$sender  = (string) wpcf7_mail_replace_tags( $sender );
			$subject = (string) wpcf7_mail_replace_tags( $subject );
		}
		$this->lastOptInMail = array(
			'optin_id' => (int) $optIn->get_id(),
			'email'    => (string) $optIn->get_email(),
			'sender'   => SubmitNotice::senderAddress( $sender ),
			'subject'  => trim( wp_strip_all_tags( $subject ) ),
			'sent'     => $sent,
		);

		$this->getLogger()->info(
			$sent ? 'OptIn mail handed to the mail server via CF7' : 'OptIn mail could not be sent via CF7',
			array(
				'plugin'   => 'double-opt-in',
				'form_id'  => $formData->getFormId(),
				'optin_id' => (int) $optIn->get_id(),
			)
		);

		return $sent;
	}

	/**
	 * Make CF7's answer to a double opt-in submission tell the truth (5.8.0).
	 *
	 * CF7 answers "Thank you for your message. It has been sent." — but
	 * nothing is sent until the address is confirmed, and when the
	 * confirmation mail itself failed CF7 still said so and cleared the form.
	 * CF7 overwrites any response set during the submission, so this runs on
	 * the finished feedback response.
	 *
	 * @param mixed $response CF7 feedback response.
	 * @param mixed $result   CF7 submission result.
	 *
	 * @return mixed
	 */
	public function filterFeedbackResponse( $response, $result = null ) {
		if ( ! is_array( $response ) || $this->submitted === null ) {
			return $response;
		}

		$formId = (int) ( $response['contact_form_id'] ?? 0 );
		if ( $formId !== $this->submitted['form_id'] || ( $response['status'] ?? '' ) !== 'mail_sent' ) {
			return $response;
		}

		$submitted       = $this->submitted;
		$this->submitted = null;
		$form            = class_exists( '\WPCF7_ContactForm' ) ? \WPCF7_ContactForm::get_instance( $formId ) : null;

		if ( ! $submitted['sent'] ) {
			// CF7 then keeps the visitor's input and fires wpcf7mailfailed.
			$response['status']  = 'mail_failed';
			$response['message'] = $form ? (string) $form->message( 'mail_sent_ng' ) : __( 'The confirmation mail could not be sent. Please try again later.', 'double-opt-in' );
			return $response;
		}

		/**
		 * Whether to show the confirmation hint (masked address, what to look
		 * for, "open your inbox" link) after a double opt-in submission.
		 *
		 * @since 5.8.0
		 *
		 * @param bool $show   Default true.
		 * @param int  $formId Form ID.
		 */
		if ( ! apply_filters( 'f12_doi_submit_notice', true, $formId ) ) {
			return $response;
		}

		$notice = SubmitNotice::extend(
			SubmitNotice::build( $submitted['email'], $submitted['sender'], $submitted['subject'] ),
			array(
				'form_id'     => $formId,
				'optin_id'    => (int) ( $submitted['optin_id'] ?? 0 ),
				'integration' => 'cf7',
			)
		);

		// A message the site owner wrote stays; only CF7's own default is wrong.
		if ( $form && self::isDefaultSentMessage( (string) $form->message( 'mail_sent_ok', false ) ) ) {
			$response['message'] = SubmitNotice::message( $notice['masked'] );
		}

		$response['doi'] = $notice;

		return $response;
	}

	/**
	 * Whether a form's success message is still CF7's default, in English or
	 * in the current language.
	 */
	public static function isDefaultSentMessage( string $message ): bool {
		$defaults = array( 'Thank you for your message. It has been sent.' );
		if ( function_exists( 'wpcf7_messages' ) ) {
			$messages   = wpcf7_messages();
			$defaults[] = (string) ( $messages['mail_sent_ok']['default'] ?? '' );
		}

		return in_array( trim( $message ), array_filter( $defaults ), true );
	}

	/**
	 * Whether a form gets the honeypot and the minimum fill time.
	 */
	private function trapEnabled( int $formId ): bool {
		/**
		 * Whether a double opt-in form gets the honeypot and the minimum
		 * fill time.
		 *
		 * @since 5.8.0
		 *
		 * @param bool $enabled Default true.
		 * @param int  $formId  Form ID.
		 */
		return $formId > 0 && $this->isOptInEnabled( $formId ) && (bool) apply_filters( 'f12_doi_spam_trap', true, $formId );
	}

	/**
	 * Add the trap fields to a double opt-in form.
	 *
	 * @param mixed $elements Form HTML.
	 *
	 * @return mixed
	 */
	public function addSubmissionTrap( $elements ) {
		if ( ! is_string( $elements ) || ! function_exists( 'wpcf7_get_current_contact_form' ) ) {
			return $elements;
		}
		$form = wpcf7_get_current_contact_form();
		if ( ! $form || ! $this->trapEnabled( (int) $form->id() ) ) {
			return $elements;
		}

		return $elements . SubmissionTrap::markup( time() );
	}

	/**
	 * Mark a bot submission as spam before any opt-in or mail exists.
	 *
	 * @param mixed $spam       CF7's verdict so far.
	 * @param mixed $submission The submission.
	 *
	 * @return mixed
	 */
	public function checkSubmissionTrap( $spam, $submission = null ) {
		if ( $spam || self::isReplaying() || ! is_object( $submission ) || ! method_exists( $submission, 'get_contact_form' ) ) {
			return $spam;
		}
		$form   = $submission->get_contact_form();
		$formId = $form ? (int) $form->id() : 0;
		if ( ! $this->trapEnabled( $formId ) ) {
			return $spam;
		}

		/**
		 * Minimum seconds between rendering a double opt-in form and
		 * submitting it; faster submissions are treated as bots.
		 *
		 * @since 5.8.0
		 *
		 * @param int $seconds Default 2.
		 * @param int $formId  Form ID.
		 */
		$minSeconds = (int) apply_filters( 'f12_doi_min_fill_seconds', SubmissionTrap::DEFAULT_MIN_SECONDS, $formId );

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- CF7 verified the submission; only our two trap fields are read.
		$reason = SubmissionTrap::check( wp_unslash( $_POST ), time(), $minSeconds );
		if ( $reason === '' ) {
			return $spam;
		}

		if ( method_exists( $submission, 'add_spam_log' ) ) {
			$submission->add_spam_log(
				array(
					'agent'  => 'double-opt-in',
					'reason' => $reason,
				)
			);
		}

		/**
		 * A double opt-in submission was stopped by the honeypot or the
		 * minimum fill time. No personal data is passed.
		 *
		 * @since 5.8.0
		 *
		 * @param string $reason      'honeypot', 'stamp_invalid' or 'too_fast'.
		 * @param int    $formId      Form ID.
		 * @param string $integration Integration identifier.
		 */
		do_action( 'f12_doi_spam_trap_hit', $reason, $formId, 'cf7' );

		$this->getLogger()->info(
			'Submission stopped by the spam trap',
			array(
				'plugin'  => 'double-opt-in',
				'form_id' => $formId,
				'reason'  => $reason,
			)
		);

		return true;
	}

	/**
	 * The script that renders the confirmation hint under the CF7 message.
	 * Runs only where CF7 enqueues its own scripts.
	 *
	 * @return void
	 */
	public function enqueueSubmitNotice(): void {
		$version = defined( 'FORGE12_OPTIN_VERSION' ) ? FORGE12_OPTIN_VERSION : false;

		wp_enqueue_script(
			'f12-doi-submit-notice',
			plugins_url( 'assets/js/doi-submit-notice.js', F12_DOUBLEOPTIN_PLUGIN_FILE ),
			array(),
			$version,
			true
		);

		wp_register_style( 'f12-doi-submit-notice', false, array(), $version );
		wp_enqueue_style( 'f12-doi-submit-notice' );
		wp_add_inline_style(
			'f12-doi-submit-notice',
			'.f12-doi-notice{margin:.5em 0 1em;padding:0 1em}'
			. '.f12-doi-notice p{margin:.4em 0}'
			. '.f12-doi-notice .f12-doi-inbox{display:inline-block;margin-top:.4em;padding:.5em 1em;border:1px solid currentColor;border-radius:4px;text-decoration:none;font-weight:600}'
			// Buttons and links of add-ons (f12_doi_submit_notice_data) look like
			// the inbox link instead of a bare browser button.
			. '.f12-doi-notice .f12-doi-action{display:inline-block;margin-top:.4em;padding:.5em 1em;border:1px solid currentColor;border-radius:4px;background:transparent;color:inherit;font:inherit;font-weight:600;text-decoration:none;cursor:pointer}'
			. '.f12-doi-notice button.f12-doi-action:disabled{opacity:.5;cursor:default}'
		);
	}

	/**
	 * {@inheritdoc}
	 */
	public function sendConfirmationMail( OptIn $optIn ): void {
		self::runAsReplay(
			function () use ( $optIn ) {
				$this->replaySubmission( $optIn );
			}
		);
	}

	/**
	 * Listener on the global `f12_cf7_doubleoptin_trigger_default_mail`.
	 * That action fires for every integration's opt-in; this used to run
	 * the CF7 submission for Elementor opt-ins too, overwriting $_POST.
	 * Managed opt-ins go through the follow-up coordinator, so a second
	 * trigger never sends twice.
	 *
	 * @param OptIn $optIn The confirmed opt-in.
	 *
	 * @since 5.6.0
	 */
	public function onTriggerDefaultMail( OptIn $optIn ): void {
		if ( ! $optIn->isType( $this->getIdentifier() ) ) {
			return;
		}

		$coordinator = FollowUpCoordinator::instance();
		if ( $coordinator !== null && $coordinator->plan( $optIn, true ) ) {
			$coordinator->run( $optIn, FollowUpAttempt::TRIGGER_LEGACY );
			return;
		}

		$this->sendConfirmationMail( $optIn );
	}

	/**
	 * Re-run the stored submission through CF7 and report what CF7 says.
	 *
	 * Must run inside {@see runAsReplay()} so our own
	 * `wpcf7_before_send_mail` listener processes it as the confirmed
	 * submission (attachments) instead of creating a new opt-in.
	 *
	 * @since 5.6.0
	 */
	public function replaySubmission( OptIn $optIn ): FollowUpResult {
		if ( ! $this->isAvailable() || ! class_exists( '\\WPCF7_ContactForm' ) || ! class_exists( '\\WPCF7_Submission' ) ) {
			$this->getLogger()->warning(
				'CF7 not available for confirmation mail',
				array(
					'plugin' => 'double-opt-in',
				)
			);
			return FollowUpResult::failedRetryable( 'integration_unavailable' );
		}

		$contactForm = \WPCF7_ContactForm::get_instance( $optIn->get_cf_form_id() );
		if ( ! $contactForm ) {
			$this->getLogger()->warning(
				'CF7 form not found for confirmation mail',
				array(
					'plugin'  => 'double-opt-in',
					'form_id' => $optIn->get_cf_form_id(),
				)
			);
			return FollowUpResult::failedPermanent( 'form_missing' );
		}

		$data = maybe_unserialize( $optIn->get_content() );
		if ( ! is_array( $data ) ) {
			return FollowUpResult::failedPermanent( 'payload_missing' );
		}

		$previousPost       = $_POST;
		$previousOptIn      = $this->currentOptIn;
		$this->currentOptIn = $optIn;
		$status             = '';

		try {
			$_POST = SanitizeHelper::sanitize_array( $data );

			// Disable validation and spam checks before creating submission
			$this->beforeSendConfirmationMail();

			// Create submission and send mail. Attachments are added by
			// onSubmit() → attachStoredFiles() while isReplaying().
			$submission = \WPCF7_Submission::get_instance( $contactForm );

			if ( is_object( $submission ) && method_exists( $submission, 'get_status' ) ) {
				$status = (string) $submission->get_status();
			}
		} finally {
			// Re-enable validation and spam checks, restore request state.
			$this->afterSendConfirmationMail();
			$_POST              = $previousPost;
			$this->currentOptIn = $previousOptIn;
		}

		$this->getLogger()->info(
			'Confirmation mail triggered via CF7',
			array(
				'plugin'     => 'double-opt-in',
				'form_id'    => $optIn->get_cf_form_id(),
				'optin_id'   => $optIn->get_id(),
				'cf7_status' => $status,
			)
		);

		return CF7FollowUpAdapter::mapStatus( $status );
	}

	/**
	 * Handle opt-in confirmation from URL.
	 *
	 * @return void
	 */
	public function handleOptInConfirmation(): void {
		if ( ! isset( $_GET['optin'] ) ) {
			return;
		}

		$hash = sanitize_text_field( $_GET['optin'] );
		$this->validateOptIn( $hash );
	}

	/**
	 * Before sending default mail callback.
	 *
	 * @return void
	 */
	public function beforeSendDefaultMail(): void {
		$this->beforeSendConfirmationMail();
	}

	/**
	 * After sending default mail callback.
	 *
	 * @return void
	 */
	public function afterSendDefaultMail(): void {
		$this->afterSendConfirmationMail();
	}

	/**
	 * Attach extra attachments to the mail.
	 *
	 * @param \WPCF7_ContactForm $form       The contact form.
	 * @param bool               $abort      Whether to abort.
	 * @param \WPCF7_Submission  $submission The submission.
	 *
	 * @return void
	 */
	public function attachExtraAttachments( $form, $abort, $submission ): void {
		if ( $this->currentOptIn && $this->currentOptIn->get_files() ) {
			$files = maybe_unserialize( $this->currentOptIn->get_files() );
			if ( is_array( $files ) ) {
				foreach ( $files as $file ) {
					$submission->add_extra_attachments( $file );
				}
			}
		}
	}

	/**
	 * Attach stored files to submission during confirmation.
	 *
	 * @param \WPCF7_Submission $submission The submission.
	 *
	 * @return void
	 */
	private function attachStoredFiles( $submission ): void {
		// The opt-in being replayed — not the one named in the URL, which
		// a cron or admin retry does not have (and a visitor controls).
		$optIn = $this->currentOptIn;

		if ( ! $optIn ) {
			return;
		}

		$files = maybe_unserialize( $optIn->get_files() );
		if ( empty( $files ) ) {
			return;
		}

		foreach ( $files as $file ) {
			if ( empty( $file ) ) {
				continue;
			}

			if ( apply_filters( 'f12_cf7_doubleoptin_files_mail_1', true, $optIn ) ) {
				$submission->add_extra_attachments( $file );
			}

			if ( apply_filters( 'f12_cf7_doubleoptin_files_mail_2', true, $optIn ) ) {
				$submission->add_extra_attachments( $file, 'mail_2' );
			}
		}
	}

	/**
	 * File hand-off — for CF7, the "form system" is the confirmation
	 * mail itself. The hand-off completes when `attachExtraAttachments`
	 * has added the pending files as mail attachments and CF7 has sent
	 * the mail. By the time `cleanupPendingAfterMail` invokes the
	 * template-method (registered on `after_send_default_mail`), this
	 * has already happened and returning true triggers the pending/
	 * cleanup — single source of truth = the recipient's mailbox.
	 *
	 * Differs from WPForms/GF (where the integration's own DB owns the
	 * file URL) in WHEN the hand-off happens, not WHAT it returns. The
	 * hook timing in registerHooks() is the actual difference.
	 *
	 * {@inheritdoc}
	 */
	public function handOffFilesToFormSystem( OptIn $optIn ): bool {
		return true;
	}

	/**
	 * CF7-specific cleanup wrapper. Hooks the file-lifecycle template-
	 * method onto `f12_cf7_doubleoptin_after_send_default_mail` rather
	 * than `f12_cf7_doubleoptin_after_confirm` (the timing the other
	 * integrations use), so pending files survive long enough for
	 * `attachExtraAttachments` to attach them to the confirmation mail.
	 *
	 * Edge case: if `f12_cf7_doubleoptin_send_default_mail` filter is
	 * false (admin opted out of the CF7 confirmation mail), this hook
	 * never fires and pending/ stays populated until the OptIn-deletion
	 * cron sweeps it via cascade-delete. Acceptable per the plan's
	 * fault-tolerance pattern — no silent data loss, just delayed
	 * cleanup.
	 *
	 * @param OptIn $optIn The just-confirmed opt-in (action arg).
	 *
	 * @since 4.3.0
	 */
	public function cleanupPendingAfterMail( OptIn $optIn ): void {
		// Managed opt-ins: CF7FollowUpAdapter::onSettled() cleans up only
		// once the mail was actually handed over. This hook fires after
		// the attempt regardless of its outcome.
		$coordinator = FollowUpCoordinator::instance();
		if ( $coordinator !== null && $coordinator->adapterFor( $optIn ) !== null ) {
			return;
		}

		// Template-method's $hash arg is unused inside processFilesOnConfirm;
		// passing an empty string keeps the contract tight.
		$this->processFilesOnConfirm( '', $optIn );
	}

	/**
	 * {@inheritdoc}
	 */
	public function getFormFields( $formId ): array {
		$formId = (int) $formId;
		$post   = get_post( $formId );
		if ( ! $post || $post->post_type !== 'wpcf7_contact_form' ) {
			return array();
		}

		$contactForm = \WPCF7_ContactForm::get_instance( $formId );
		if ( ! $contactForm ) {
			return array();
		}

		$fields = array();
		$tags   = $contactForm->scan_form_tags();

		foreach ( $tags as $tag ) {
			if ( ! empty( $tag->name ) ) {
				$fields[ $tag->name ] = $tag->name;
			}
		}

		return $fields;
	}

	/**
	 * Add editor panel to CF7.
	 *
	 * @param array $panels The panels array.
	 *
	 * @return array Modified panels.
	 */
	public function addEditorPanel( array $panels ): array {
		$panels['optin'] = array(
			'title'    => $this->getPanelTitle(),
			'callback' => array( $this, 'renderPanel' ),
		);
		return $panels;
	}

	/**
	 * {@inheritdoc}
	 */
	public function render( $form, array $metadata ): void {
		$this->renderPanel( $form );
	}

	/**
	 * Render the CF7 editor panel.
	 *
	 * Displays a notice with link to central form management.
	 * Full settings are now managed centrally in the Forms admin page.
	 *
	 * @param \WPCF7_ContactForm $post The contact form.
	 *
	 * @return void
	 */
	public function renderPanel( $post ): void {
		if ( ! $post || ! $post->id() ) {
			?>
			<div class="doi-cf7-notice" style="padding: 20px;">
				<div style="background: #fff; border: 1px solid #c3c4c7; border-radius: 4px; padding: 20px;">
					<h2 style="margin-top: 0;"><?php _e( 'Double Opt-In Settings', 'double-opt-in' ); ?></h2>
					<p style="color: #666;">
						<?php _e( 'Please save the contact form first before configuring Double Opt-In.', 'double-opt-in' ); ?>
					</p>
				</div>
			</div>
			<?php
			return;
		}

		$metadata   = $this->getFormParameter( $post->id() );
		$centralUrl = admin_url( 'admin.php?page=f12-doi-admin#/forms' );
		$isEnabled  = $this->isOptInEnabled( $post->id() );

		$this->getLogger()->debug(
			'Rendering CF7 panel notice',
			array(
				'plugin'  => 'double-opt-in',
				'form_id' => $post->id(),
				'enabled' => $isEnabled,
			)
		);
		?>
		<div class="doi-cf7-notice" style="padding: 20px;">
			<div style="background: #fff; border: 1px solid #c3c4c7; border-radius: 4px; padding: 20px;">
				<h2 style="margin-top: 0;"><?php _e( 'Double Opt-In Settings', 'double-opt-in' ); ?></h2>

				<div style="display: flex; align-items: center; gap: 15px; margin-bottom: 20px;">
					<span style="font-weight: 600;"><?php _e( 'Status:', 'double-opt-in' ); ?></span>
					<?php if ( $isEnabled ) : ?>
						<span style="display: inline-block; padding: 4px 12px; background: #d4edda; color: #155724; border-radius: 3px; font-weight: 500;">
							<?php _e( 'Enabled', 'double-opt-in' ); ?>
						</span>
					<?php else : ?>
						<span style="display: inline-block; padding: 4px 12px; background: #f8d7da; color: #721c24; border-radius: 3px; font-weight: 500;">
							<?php _e( 'Disabled', 'double-opt-in' ); ?>
						</span>
					<?php endif; ?>
				</div>

				<p style="color: #666; margin-bottom: 20px;">
					<?php _e( 'Double Opt-In settings are now managed centrally. Use the button below to configure this form.', 'double-opt-in' ); ?>
				</p>

				<a href="<?php echo esc_url( $centralUrl ); ?>" class="button button-primary" target="_blank">
					<?php _e( 'Configure Double Opt-In', 'double-opt-in' ); ?>
				</a>
			</div>
		</div>
		<?php
	}

	/**
	 * {@inheritdoc}
	 */
	public function save( int $formId, array $data ): bool {
		if ( ! isset( $data['doubleoptin'] ) ) {
			update_post_meta( $formId, 'f12-cf7-doubleoptin', array() );
			return true;
		}

		$parameter = SanitizeHelper::sanitize_array( $data['doubleoptin'] );
		$metadata  = $this->getFormParameter( $formId );

		foreach ( $metadata as $key => $value ) {
			if ( isset( $parameter[ $key ] ) ) {
				$metadata[ $key ] = $key === 'enable' ? (int) $parameter[ $key ] : $parameter[ $key ];
			} elseif ( $key === 'enable' ) {
				$metadata[ $key ] = 0;
			}
		}

		$metadata = apply_filters( 'f12_cf7_doubleoptin_metadata_cf7', $metadata );
		$metadata = apply_filters( 'f12_cf7_doubleoptin_save_form', $metadata );

		update_post_meta( $formId, 'f12-cf7-doubleoptin', $metadata );

		// Save placeholder mapping
		if ( isset( $data['doubleoptin']['placeholder_mapping'] ) ) {
			$mapping = array_map( 'sanitize_text_field', $data['doubleoptin']['placeholder_mapping'] );
			PlaceholderMapper::saveCustomMapping( $formId, $mapping, 'cf7' );
		}

		return true;
	}

	/**
	 * Save form settings callback.
	 *
	 * @param \WPCF7_ContactForm $contactForm The contact form.
	 * @param array              $args        The arguments.
	 * @param string             $context     The context.
	 *
	 * @return void
	 */
	public function saveFormSettings( $contactForm, $args, $context ): void {
		$formId = $contactForm->id();

		// Verify nonce
		if ( ! isset( $_POST['f12_cf7_doubleoptin_save_form_nonce'] ) ||
			! wp_verify_nonce( wp_unslash( $_POST['f12_cf7_doubleoptin_save_form_nonce'] ), 'f12_cf7_doubleoptin_save_form_action' ) ) {
			return;
		}

		$this->save( $formId, $_POST );
	}

	/**
	 * {@inheritdoc}
	 */
	public function getPanelTitle(): string {
		return __( 'Double-Opt-in', 'double-opt-in' );
	}

	/**
	 * {@inheritdoc}
	 */
	public function getAvailableTemplates(): array {
		$templates = array(
			'blank'           => 'blank',
			'newsletter_en'   => 'newsletter_en',
			'newsletter_en_2' => 'newsletter_en_2',
			'newsletter_en_3' => 'newsletter_en_3',
		);

		// Add custom templates
		try {
			$container   = Container::getInstance();
			$integration = $container->get( \Forge12\DoubleOptIn\EmailTemplates\EmailTemplateIntegration::class );
			$custom      = $integration->getCustomTemplates();

			foreach ( $custom as $template ) {
				$templates[ 'custom_' . $template['id'] ] = $template['title'] . ' (' . __( 'Custom', 'double-opt-in' ) . ')';
			}
		} catch ( \Exception $e ) {
			// Ignore if custom templates not available
		}

		return $templates;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getAvailableCategories(): array {
		$categories = array( 0 => __( 'Please select', 'double-opt-in' ) );

		$list = Category::get_list(
			array(
				'perPage' => -1,
				'orderBy' => 'name',
				'order'   => 'ASC',
			),
			$numberOfPages
		);

		foreach ( $list as $category ) {
			$categories[ $category->get_id() ] = $category->get_name();
		}

		return $categories;
	}
}

```
