Extend user search in the Wp backend area on the users.php page to allow for searching by email domain and role from the “users search” input box

You can do this more simply by using the pre_get_users hook, grab the search string extract the role from it and add that as a role parameter for WP_User_Query.

Edited my original code, here’s a refined version, bit cleaner.

Additional edit: Scroll further down for update (see follow-up).

class wpse_410251_user_search {
    
    private $role = false;
    private $search="";
    
    public function __construct() { 
        add_action( 'admin_init', array( $this, 'admin_init' ) );
    }
    
    public function admin_init() {
        add_action( 'pre_get_users', array( $this, 'pre_get_users' ) );
    }
    
    public function pre_get_users( $query ) {
        
        // $query is a WP_User_Query object
        
        global $pagenow;    
        
        // If not admin or the user listing
        if( !is_admin() || 'users.php' !== $pagenow )
            return;
        
        // If not a search
        if( empty( $query->get('search') ) )
            return;
        
        // Do escaping early and trim off the wildcards
        $search = trim( esc_attr( $query->get('search') ),'*' );
        
        // Check the search for appropriate search terms and set class vars, returns true/false as appropriate
        if( !$this->is_role_search( $search ) )
            return;
        
        // Update the search query var (adding back the wildcards)
        $query->set( 'search', "*{$this->search}*" );
        // Set the role query var
        $query->set( 'role', $this->role );
    }
    private function is_role_search( $search ) {
        
        // Split the search on space
        $search = explode( ' ', $search );
        
        // If no second search term or @ isn't the first search character
        if( !isset( $search[1] ) || '@' !== mb_substr( $search[0], 0, 1 ) )
            return false;
        
        // Bring the WP roles into scope
        global $wp_roles;
        
        // Use the $wp_roles->role_names array to iterate over and see if we get a match to an existing role
        foreach( $wp_roles->role_names as $role => $name ) {
            // Check against the lowercase slug and the display name
            if( $search[1] === $role || $search[1] === $name )
                // Store the matched role
                $this->role = $role;
        }
        // If no role set for the class, there's no match, so not a role search
        if( !$this->role )
            return false;
        
        // Store the first part of the search for updating the search query var
        $this->search = $search[0];
        
        // Else we reach here, a role has been set, return true
        return true;
    }
}
$wpse_410251_user_search = new wpse_410251_user_search;

Shame there’s no convenient action hook on the users page to insert a dropdown next to the search box, it would have been nice to build it into a dropdown selection so you can select a role as you search (instead of having to type it in).

Now you can search on the users page using, for example @yahoo Subscriber or @gmail.com author, and so on. You can use the role slug or display name, it will work with either case.

If you want to limit this all to only working for admins, simply add a if( !current_user_can( 'manage_options' ) ) return; or similar check inside the pre_get_users function.

Follow-up:

You asked about adding in a notin: parameter so you can filter your users by role where their email isn’t matching. This isn’t as clean as the above code, because there’s no built-in query vars with WP_User_Query to say search the user fields such as name, email, login, etc, with a NOT LIKE match.

It is possible however to add an additional hook onto pre_user_query to tweak the query_where data before the query runs and perform a string replacement on the part of the query that checks the user’s email, simply swapping the LIKE portion to NOT LIKE so it does an inverted match.

I took this opporunity to also switch around the order of parameters, so it’s Role :type @email.

Searching for users with a role and email matching a domain is Subscriber in: @gmail.com and users with a role whose email doesn’t match the domain is Subscriber notin: @gmail.com.

class wpse_410251_user_search {
    
    private $role = false;
    private $update_where = false;
    private $search="";
    
    public function __construct() { 
        add_action( 'admin_init', array( $this, 'admin_init' ) );
    }
    
    public function admin_init() {
        add_action( 'pre_get_users', array( $this, 'pre_get_users' ) );
        add_action( 'pre_user_query', array( $this, 'pre_user_query' ) );
    }
    public function pre_user_query( $query ){
        if( $this->update_where )
            $query->query_where = str_replace( 'user_email LIKE', 'user_email NOT LIKE', $query->query_where );
    }
    
    public function pre_get_users( $query ) {
        
        // $query is a WP_User_Query object
        
        global $pagenow;    
        
        // If not admin or the user listing
        if( !is_admin() || 'users.php' !== $pagenow )
            return;
        
        // If not a search
        if( empty( $query->get('search') ) )
            return;
        
        // Do escaping early and trim off the wildcards
        $search = trim( esc_attr( $query->get('search') ),'*' );
        
        // Check the search for appropriate search terms and set class vars, returns true/false as appropriate
        if( !$this->is_role_search( $search ) )
            return;
        
        // Update the search query var (adding back the wildcards), less the role and in/notin parts
        $query->set( 'search', "*{$this->search}*" );
        // Set the role query var
        $query->set( 'role', $this->role );
    }
    private function is_role_search( $search_terms ) {
        
        // Bail early if there's no @ character
        if( false === strpos( $search_terms, '@' ) )
            return false;
        
        // Otherwise move along and explode the search
        $search_terms = explode( ' ', $search_terms );      
        
        // There should be 3 parameters for our use case
        if( 3 !== count( $search_terms ) )
            return false;
        /*
            in: no query_where changes
            notin: set a flag and adjust query_where
            
            This keeps the array structure consistent
        
            [0] = role 
            [1] = type (in/not in)
            [2] = @example.com 
            
            Example use:
            Subscriber notin: @wordpress.com
            Author in: @gmail.com
        */
        
        // Check one of the two allowed values is provided
        if( !in_array( $search_terms[1], array( 'in:', 'notin:' ) ) )
            return false;
        
        // Bring the WP roles into scope
        global $wp_roles;
        
        // Using the $wp_roles->role_names array to iterate over and find a matching role
        foreach( $wp_roles->role_names as $role => $name ) {
            // Check against the lowercase slug and the display name
            if( $search_terms[0] === $role || $search_terms[0] === $name )
                // Store the matched role
                $this->role = $role;
        }
        
        // If no role set return false
        if( !$this->role )
            return false;
        
        // Only need to change pre_user_query when using notin:
        if( 'notin:' == $search_terms[1] )
            $this->update_where = true;

        // The @email portion of the search
        $this->search = $search_terms[2];
        
        // If we're still here, return true
        return true;
    }
}
$wpse_410251_user_search = new wpse_410251_user_search;

If i were building something like this for a project myself i’d probably look at using some jQuery/js to insert extra form elements into the user listing so it can be managed easier with select boxes instead of having to use the search field.

Was a fun little exericse though. Hope that helps.