A custom client converter or a validator can be implemented easily. Both JSF and Bean Validation APIs are supported.
<script>
    //<![CDATA[
    /**
     * Faces Validator
     */
    PrimeFaces.validator['custom.emailValidator'] = {
        pattern: /\S+@\S+/,
        validate: function (element, value) {
            //use element.data() to access validation metadata, in this case there is none.
            if (!this.pattern.test(value)) {
                throw {
                    summary: 'Validation Error',
                    detail: value + ' is not a valid email.',
                    severity: 'error'
                }
            }
        }
    };
    /**
     * Bean validator
     */
    PrimeFaces.validator['Email'] = {
        pattern: /\S+@\S+/,
        MESSAGE_ID: 'org.primefaces.examples.validate.email.message',
        validate: function (element, value) {
            var vc = PrimeFaces.validation.ValidationContext;
            if (!this.pattern.test(value)) {
                var msgStr = element.data('p-email-msg'),
                    msg = msgStr ? {summary: 'Validation Error', detail: msgStr, severity: 'error'} : vc.getMessage(this.MESSAGE_ID);
                throw msg;
            }
        }
    };
    //]]>
</script>
<div class="card">
    <h:form>
        <p:growl showDetail="true"/>
        <h:panelGrid columns="4" cellpadding="7">
            <p:outputLabel for="email1" value="Email 1: (JSF Validation)"/>
            <p:inputText id="email1" value="#{customValidationView.text}">
                <f:validator validatorId="custom.emailValidator"/>
            </p:inputText>
            <p:message for="email1" display="tooltip"/>
            <h:outputText value="#{customValidationView.text}"/>
            <p:outputLabel for="email2" value="Email 2: (Bean Validation)"/>
            <p:inputText id="email2" value="#{customValidationView.email}"/>
            <p:message for="email2" display="tooltip"/>
            <h:outputText value="#{customValidationView.email}"/>
        </h:panelGrid>
        
        <p:commandButton value="Save" ajax="false" icon="pi pi-check" validateClient="true" styleClass="mt-3" />
    </h:form>
</div>
package org.primefaces.showcase.view.csv;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
@Named
@RequestScoped
public class CustomValidationView {
    private String text;
    @Email(message = "must be a valid email")
    private String email;
    public String getText() {
        return text;
    }
    public void setText(String text) {
        this.text = text;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
}
package org.primefaces.showcase.view.csv;
import org.primefaces.validate.ClientValidator;
import java.util.Map;
import java.util.regex.Pattern;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.validator.FacesValidator;
import jakarta.faces.validator.Validator;
import jakarta.faces.validator.ValidatorException;
/**
 * Custom JSF Validator for Email input
 */
@FacesValidator("custom.emailValidator")
public class EmailValidator implements Validator, ClientValidator {
    private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
            + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
    private Pattern pattern;
    public EmailValidator() {
        pattern = Pattern.compile(EMAIL_PATTERN);
    }
    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
        if (value == null) {
            return;
        }
        if (!pattern.matcher(value.toString()).matches()) {
            throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "Validation Error",
                    value + " is not a valid email;"));
        }
    }
    @Override
    public Map<String, Object> getMetadata() {
        return null;
    }
    @Override
    public String getValidatorId() {
        return "custom.emailValidator";
    }
}
package org.primefaces.showcase.view.csv;
import org.primefaces.validate.bean.ClientConstraint;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EmailConstraintValidator.class)
@ClientConstraint(resolvedBy = EmailClientValidationConstraint.class)
@Documented
public @interface Email {
    String message() default "{org.primefaces.examples.primefaces}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
package org.primefaces.showcase.view.csv;
import java.util.regex.Pattern;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
/**
 * ConstraintValidator for @Email
 */
public class EmailConstraintValidator implements ConstraintValidator<Email, String> {
    private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
            + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
    private Pattern pattern;
    @Override
    public void initialize(Email a) {
        pattern = Pattern.compile(EMAIL_PATTERN);
    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext cvc) {
        if (value == null) {
            return true;
        }
        else {
            return pattern.matcher(value).matches();
        }
    }
}
package org.primefaces.showcase.view.csv;
import org.primefaces.validate.bean.AbstractClientValidationConstraint;
/**
 * ClientValidationConstraint for @Email annotation
 */
public class EmailClientValidationConstraint extends AbstractClientValidationConstraint {
    public static final String MESSAGE_METADATA = "data-p-email-msg";
    public EmailClientValidationConstraint() {
        super(null, MESSAGE_METADATA);
    }
    @Override
    public String getValidatorId() {
        return Email.class.getSimpleName();
    }
}