# contact-forms/trunk/includes/spam-blocklist.php

Contact Forms by Cimatti, version trunk. 388 lines.

- Page: https://pluginprobe.com/plugins/contact-forms/trunk/code/includes/spam-blocklist.php
- Raw: https://pluginprobe.com/plugins/contact-forms/trunk/raw/includes/spam-blocklist.php
- Modified: 2026-08-21T08:39:40+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/contact-forms/trunk/code/includes/spam-blocklist.php#L10-L20`.

```php
<?php
/**
 * Auto mark as spam - email address blocklist.
 *
 * A site-wide list of email addresses and wildcard patterns. A submission that
 * carries one of them in an email field is classified as spam by the
 * submission handler whatever the captcha of that form decided: a captcha
 * judges how a form was filled in, not who filled it in, so a known spammer
 * who solves the challenge correctly still has to be caught.
 *
 * The list is stored as the text the administrator typed (one entry per line,
 * '#' comment lines kept) rather than as an array, so the settings textarea can
 * round-trip it unchanged and the order and the comments survive a save.
 *
 * Deliberately hook-free - plain functions only, like includes/data-deletion.php
 * - and cheap when nothing is configured: the scan returns before it looks at a
 * single submitted value if the list is empty.
 *
 * @package Contact Forms
 * @since 2.3.1
 */

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

/**
 * The stored spam settings, with every key guaranteed present.
 *
 * @since 2.3.1
 * @return array The settings, with at least the 'email_blocklist' key.
 */
function accua_forms_spam_settings() {
	$defaults = array(
		'email_blocklist' => '',
	);
	$data = get_option( 'accua_forms_spam_data', array() );
	if ( ! is_array( $data ) ) {
		$data = array();
	}
	$data = $data + $defaults;

	// The list is stored as text, but an option written by hand, by a
	// migration or by an older shape can hold an array. Casting that to
	// string raises "Array to string conversion" in every caller, so fold it
	// back into lines instead of losing it.
	if ( is_array( $data['email_blocklist'] ) ) {
		$entries = array();
		foreach ( $data['email_blocklist'] as $entry ) {
			if ( is_scalar( $entry ) ) {
				$entries[] = (string) $entry;
			}
		}
		$data['email_blocklist'] = implode( "\n", $entries );
	} elseif ( ! is_string( $data['email_blocklist'] ) ) {
		$data['email_blocklist'] = is_scalar( $data['email_blocklist'] ) ? (string) $data['email_blocklist'] : '';
	}

	return $data;
}

/**
 * Clean up a single blocklist entry.
 *
 * Accepts what people actually paste - "Spam Bot <bot@example.net>",
 * "mailto:bot@example.net" - and lowercases everything, since addresses are
 * matched case-insensitively.
 *
 * Returns '' for anything unusable, which includes the entries that would
 * silently mark every submission carrying an email address as spam: an entry
 * made only of wildcards and '@' ('*', '*@*', '?') is dropped rather than
 * stored. Blocking everything stays expressible per domain ('*@example.com'),
 * never by accident.
 *
 * @since 2.3.1
 * @param string $entry One entry as typed.
 * @return string The normalized entry, or '' if it cannot be used.
 */
function accua_forms_normalize_blocklist_entry( $entry ) {
	$entry = trim( (string) $entry );
	if ( '' === $entry ) {
		return '';
	}

	// "Display Name <address>" - keep the address. The trim() after it catches
	// an unbalanced bracket, which the pattern above does not match.
	if ( preg_match( '/<([^>]*)>/', $entry, $matches ) ) {
		$entry = $matches[1];
	}
	$entry = trim( $entry, " \t<>" );
	$entry = preg_replace( '/^mailto:/i', '', $entry );
	$entry = strtolower( trim( $entry ) );

	// Characters no address and no pattern over one can contain. Whitespace,
	// commas and semicolons are already gone (they separate entries), so what
	// is left here is a typo that could never match anything.
	if ( '' === $entry || preg_match( '/[<>"\'\\\\()\[\]:;,\s]/', $entry ) ) {
		return '';
	}

	// A run of consecutive '*' means exactly what one '*' means, and each one
	// becomes a '.*' in the match regex: 20 of them in a row turn a failed
	// match into minutes of backtracking. Collapsing is not a restriction,
	// the pattern keeps matching precisely what it did.
	$entry = preg_replace( '/\*{2,}/', '*', $entry );

	// At least one character that is not a wildcard or the '@' itself.
	if ( ! preg_match( '/[^*?@]/', $entry ) ) {
		return '';
	}

	// An entry with no '@' is shorthand for a whole domain, so it has to look
	// like one: a dot, or a wildcard. Without this check a stray word from a
	// pasted line would become an entry of its own and be read as a domain -
	// "Spam Bot <bot@example.net>" splits on its spaces, and "spam" would then
	// match every address at a host called spam.
	if ( false === strpos( $entry, '@' )
	     && false === strpos( $entry, '.' )
	     && ! preg_match( '/[*?]/', $entry ) ) {
		return '';
	}

	return $entry;
}

/**
 * Normalize a raw blocklist textarea into the text that gets stored.
 *
 * Entries may be separated by newlines, commas, semicolons or spaces; lines
 * starting with '#' are comments and are kept verbatim (the list doubles as
 * the note of why an address is on it). Duplicates are dropped.
 *
 * @since 2.3.1
 * @param string $raw The submitted textarea content.
 * @return string One entry (or comment) per line.
 */
function accua_forms_filter_email_blocklist( $raw ) {
	$lines = array();
	$seen  = array();

	foreach ( preg_split( '/[\r\n]+/', (string) $raw ) as $line ) {
		$line = trim( $line );
		if ( '' === $line ) {
			continue;
		}
		if ( 0 === strpos( $line, '#' ) ) {
			$lines[] = $line;
			continue;
		}
		// "Display Name <address>" has to be unwrapped before the line is split
		// on its spaces, or the display name becomes entries of its own.
		if ( preg_match_all( '/<([^>]*)>/', $line, $matches ) ) {
			$line = implode( ' ', $matches[1] );
		}
		foreach ( preg_split( '/[\s,;]+/', $line ) as $entry ) {
			$entry = accua_forms_normalize_blocklist_entry( $entry );
			if ( '' === $entry || isset( $seen[ $entry ] ) ) {
				continue;
			}
			$seen[ $entry ] = true;
			$lines[]        = $entry;
		}
	}

	return implode( "\n", $lines );
}

/**
 * The blocklist as a list of patterns, without comments or blank lines.
 *
 * @since 2.3.1
 * @return array List of patterns.
 */
function accua_forms_get_email_blocklist() {
	$settings = accua_forms_spam_settings();
	$patterns = array();

	foreach ( preg_split( '/[\r\n]+/', (string) $settings['email_blocklist'] ) as $line ) {
		$line = trim( $line );
		if ( '' === $line || 0 === strpos( $line, '#' ) ) {
			continue;
		}
		$patterns[] = $line;
	}

	/**
	 * Filter the email blocklist patterns.
	 *
	 * Lets code add addresses the settings page does not hold - a list shared
	 * across a network, an external service - without touching the option.
	 *
	 * @since 2.3.1
	 * @param array $patterns Patterns read from the settings page.
	 */
	$patterns = apply_filters( 'accua_forms_email_blocklist', $patterns );

	if ( ! is_array( $patterns ) ) {
		return array();
	}

	$clean = array();
	foreach ( $patterns as $pattern ) {
		if ( is_scalar( $pattern ) && '' !== trim( (string) $pattern ) ) {
			$clean[] = trim( (string) $pattern );
		}
	}
	return $clean;
}

/**
 * Does an email address match one blocklist pattern?
 *
 * Matching is case-insensitive over the whole address, with '*' standing for
 * any run of characters and '?' for exactly one. A pattern with no '@'
 * ("spammydomain.net") and one that starts with it ("@spammydomain.net") both
 * read as "*@spammydomain.net" - the common case, spelled the way people write
 * it. Subdomains are not implied: "*@*.spammydomain.net" is how you catch them.
 *
 * @since 2.3.1
 * @param string $email   The address to test.
 * @param string $pattern One blocklist entry.
 * @return bool
 */
function accua_forms_email_matches_blocklist_pattern( $email, $pattern ) {
	$email   = strtolower( trim( (string) $email ) );
	$pattern = strtolower( trim( (string) $pattern ) );

	if ( '' === $email || '' === $pattern ) {
		return false;
	}

	// Patterns from the accua_forms_email_blocklist filter, and any stored by
	// a version before this one, never passed through the normalizer: collapse
	// the wildcard runs here too. Semantics are unchanged, there is simply
	// less to walk.
	$pattern = preg_replace( '/\*{2,}/', '*', $pattern );

	if ( false === strpos( $pattern, '@' ) ) {
		$pattern = '*@' . $pattern;
	} elseif ( 0 === strpos( $pattern, '@' ) ) {
		$pattern = '*' . $pattern;
	}

	return accua_forms_glob_matches( $pattern, $email );
}

/**
 * Glob matching without a regex.
 *
 * The obvious implementation turns the pattern into a regex ('*' to '.*',
 * '?' to '.') and calls preg_match(). That is what this did until 2.3.2, and
 * it fails quietly: every '*' becomes a backtracking point, so a pattern as
 * ordinary as '*info*@*.example.com' exhausts PCRE's backtrack limit against a
 * long address that does not match. preg_match() then returns false, not 0,
 * and the caller's '1 === preg_match(...)' reads that as "no match" - the
 * blocklist entry silently stops blocking, and only for some addresses.
 *
 * This is the standard linear glob walk instead: advance through both strings,
 * remember the last '*' and the position it was matched at, and on a mismatch
 * return to it having consumed one more character. No recursion, no
 * backtracking stack, no limit to exhaust.
 *
 * Comparison is byte-wise, so '?' matches one byte exactly as the regex '.'
 * did without the /u modifier. Both arguments are already lowercased.
 *
 * @since 2.3.2
 * @param string $pattern Glob pattern ('*' any run, '?' exactly one).
 * @param string $subject The string to test.
 * @return bool
 */
function accua_forms_glob_matches( $pattern, $subject ) {
	$plen = strlen( $pattern );
	$slen = strlen( $subject );
	$p    = 0;
	$s    = 0;
	$star = -1;
	$mark = 0;

	while ( $s < $slen ) {
		if ( $p < $plen && ( '?' === $pattern[ $p ] || $pattern[ $p ] === $subject[ $s ] ) ) {
			$p++;
			$s++;
		} elseif ( $p < $plen && '*' === $pattern[ $p ] ) {
			$star = $p;
			$mark = $s;
			$p++;
		} elseif ( $star >= 0 ) {
			$p = $star + 1;
			$mark++;
			$s = $mark;
		} else {
			return false;
		}
	}

	while ( $p < $plen && '*' === $pattern[ $p ] ) {
		$p++;
	}

	return $p === $plen;
}

/**
 * Is this email address on the blocklist?
 *
 * @since 2.3.1
 * @param string $email The address to test.
 * @return bool
 */
function accua_forms_email_is_blocklisted( $email ) {
	$email   = trim( (string) $email );
	$blocked = false;

	if ( '' !== $email ) {
		foreach ( accua_forms_get_email_blocklist() as $pattern ) {
			if ( accua_forms_email_matches_blocklist_pattern( $email, $pattern ) ) {
				$blocked = true;
				break;
			}
		}
	}

	/**
	 * Filter whether an email address is treated as spam.
	 *
	 * @since 2.3.1
	 * @param bool   $blocked Whether the blocklist matched.
	 * @param string $email   The address that was tested.
	 */
	return (bool) apply_filters( 'accua_forms_email_is_blocklisted', $blocked, $email );
}

/**
 * The field types the blocklist reads.
 *
 * @since 2.3.1
 * @return array Field type ids.
 */
function accua_forms_spam_email_field_types() {
	/**
	 * Filter which field types are scanned against the email blocklist.
	 *
	 * @since 2.3.1
	 * @param array $types Field type ids.
	 */
	$types = apply_filters( 'accua_forms_spam_email_field_types', array( 'email', 'autoreply_email' ) );

	return is_array( $types ) ? $types : array();
}

/**
 * Does this submission carry a blocklisted address in one of its email fields?
 *
 * @since 2.3.1
 * @param array $submitted_data Submitted values keyed by field instance id.
 * @param array $form_data      The saved form data (needs 'fields').
 * @return bool
 */
function accua_forms_submission_has_blocklisted_email( $submitted_data, $form_data ) {
	if ( ! is_array( $submitted_data ) || empty( $form_data['fields'] ) || ! is_array( $form_data['fields'] ) ) {
		return false;
	}

	// Nothing configured: never look at the submitted values at all.
	if ( ! accua_forms_get_email_blocklist() ) {
		return false;
	}

	$avail_fields = get_option( 'accua_forms_avail_fields', array() );
	$email_types  = accua_forms_spam_email_field_types();

	foreach ( $submitted_data as $istance_id => $value ) {
		if ( empty( $form_data['fields'][ $istance_id ]['ref'] ) ) {
			continue;
		}
		$ref = $form_data['fields'][ $istance_id ]['ref'];
		if ( empty( $avail_fields[ $ref ]['type'] )
		     || ! in_array( $avail_fields[ $ref ]['type'], $email_types, true ) ) {
			continue;
		}
		foreach ( (array) $value as $single ) {
			if ( is_scalar( $single ) && accua_forms_email_is_blocklisted( $single ) ) {
				return true;
			}
		}
	}

	return false;
}

```
