# Email validation in Java with Spring Boot

In Spring Boot, email validation usually means the `@Email` annotation from Jakarta Bean Validation. It checks the format, and it accepts more than most developers expect. This guide shows what `@Email` does and does not catch, then adds a mailbox check with the Easy Email Verification API through Spring's `RestClient`, ending with a signup controller. The code was tested with Spring Boot 3.5 and Java 17, and every example works with the free sandbox key `eev_sandbox_key`.

## 1. Check the format with @Email

Add `spring-boot-starter-validation` and annotate the request:

```java
public record SignupRequest(@NotBlank @Email String email) {}
```

With `@Valid` on the controller parameter, a badly written address such as `not-an-email` is rejected with HTTP 400 before your code runs.

Know its limits: Hibernate Validator's `@Email` accepts `jane@example`, a domain without a dot, because that form is legal in the email standard. If your users must give internet addresses, add a pattern:

```java
@Email(regexp = "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")
```

## Why format validation is not enough

All of these pass `@Email` and still bounce:

| Address | Passes @Email | Problem |
| --- | --- | --- |
| `jane@gmial.com` | yes | The domain is a typo and does not exist |
| `former.employee@example.com` | yes | The mailbox was closed |
| `jane@example-site.com` | yes | The domain has no MX record, so it receives no mail |
| `x7k2@temp-inbox.example` | yes | Disposable inbox that disappears in a few minutes |

Finding them takes a DNS lookup and an SMTP conversation with the mail server. Doing that from your own application is fragile: cloud providers often block outgoing port 25, and mail servers greylist or block unknown hosts that probe mailboxes. An email verification API does it for you, without sending any email.

## 2. Map the API response

The API returns JSON. A record with the fields you need is enough; unknown fields are ignored:

```java
@JsonIgnoreProperties(ignoreUnknown = true)
public record EmailVerification(
        String email,
        String result,
        String reason,
        boolean disposable,
        @JsonProperty("accept_all") boolean acceptAll,
        boolean role,
        @JsonProperty("safe_to_send") boolean safeToSend,
        @JsonProperty("did_you_mean") String didYouMean) {

    public String decision() {
        if (didYouMean != null && !didYouMean.isEmpty()) return "suggest"; // ask the user: did you mean ...?
        if ("valid".equals(result) && safeToSend) return "accept";
        if ("invalid".equals(result)) return "reject";
        return "review"; // unknown, catch-all or disposable: your policy decides
    }
}
```

`result` is `valid`, `invalid` or `unknown`:

- **accept**: the mail server accepted the mailbox and no risk signal advises against it.
- **reject**: the address cannot receive mail. The [reason](https://www.easyemailverification.com/en-US/help/result-codes) says why (`rejected_email`, `invalid_domain`, `no_mx_record`…).
- **suggest**: `didYouMean` has a correction, for example `typo@gmail.com` for `typo@gmial.com`.
- **review**: an [unknown result](https://www.easyemailverification.com/en-US/help/unknown), a [catch-all domain](https://www.easyemailverification.com/en-US/help/accept-all) or a [disposable address](https://www.easyemailverification.com/en-US/help/disposable). Unknown is not invalid: the server timed out or asked to try later.

## 3. Call the API with RestClient

The key comes from the `eev.api-key` property (for example `EEV_API_KEY` in the environment, mapped in `application.properties` as `eev.api-key=${EEV_API_KEY:eev_sandbox_key}`). Keep it on the server.

```java
@Component
public class EmailVerificationClient {

    private final RestClient http;

    public EmailVerificationClient(RestClient.Builder builder,
                                   @Value("${eev.api-key:eev_sandbox_key}") String apiKey) {
        this.http = builder
                .baseUrl("https://api.easyemailverification.com/v1")
                .defaultHeader("X-API-Key", apiKey)
                .build();
    }

    public EmailVerification verify(String email) {
        return http.get()
                .uri(uri -> uri.path("/verify").queryParam("email", "{email}").build(email))
                .retrieve()
                .body(EmailVerification.class);
    }

    public List<EmailVerification> verifyAll(List<String> emails) { // up to 50 per request
        return http.post()
                .uri("/verify")
                .contentType(MediaType.APPLICATION_JSON)
                .body(Map.of("emails", emails))
                .retrieve()
                .body(new ParameterizedTypeReference<List<EmailVerification>>() {});
    }
}
```

Passing the address as a URI variable (`"{email}"` plus `build(email)`) makes Spring encode it fully, so a `+` in `jane+news@example.com` reaches the API intact. Errors (`401` unknown key, `402` no credits) raise a `RestClientResponseException`.

## 4. Use it in a signup controller

The controller rejects undeliverable addresses, offers the typo correction and never blocks a signup only because the verification could not run:

```java
@RestController
public class SignupController {

    public record SignupRequest(@NotBlank @Email String email) {}

    private final EmailVerificationClient verifier;

    public SignupController(EmailVerificationClient verifier) {
        this.verifier = verifier;
    }

    @PostMapping("/signup")
    public ResponseEntity<Map<String, Object>> signup(@Valid @RequestBody SignupRequest request) {
        EmailVerification result;
        try {
            result = verifier.verify(request.email().trim());
        } catch (RestClientException e) {
            result = null; // verification unavailable: do not block the signup
        }
        String decision = result == null ? "review" : result.decision();
        return switch (decision) {
            case "reject" -> ResponseEntity.badRequest().body(Map.of("error", "This email address cannot receive mail."));
            case "suggest" -> ResponseEntity.badRequest().body(Map.of("error", "Did you mean " + result.didYouMean() + "?"));
            // "accept" or "review": create the account; for "review", confirm the address by email
            default -> ResponseEntity.ok(Map.of("ok", true, "needsConfirmation", decision.equals("review")));
        };
    }
}
```

Dependencies: `spring-boot-starter-web` and `spring-boot-starter-validation`. For whole files with thousands of addresses, use the bulk endpoints described in the [API reference](https://www.easyemailverification.com/en-US/api/reference) instead of a loop.

## Test addresses

With the key `eev_sandbox_key`, these addresses return fixed answers and use no credits:

| Address | Answer |
| --- | --- |
| `valid@sandbox.easyemailverification.com` | `valid`, `accepted_email` |
| `invalid@sandbox.easyemailverification.com` | `invalid`, `rejected_email` |
| `unknown@sandbox.easyemailverification.com` | `unknown`, `timeout` |
| `catchall@sandbox.easyemailverification.com` | `valid`, `accept_all: true`, `safe_to_send: false` |
| `disposable@sandbox.easyemailverification.com` | `valid`, `disposable: true`, `safe_to_send: false` |
| `typo@gmial.com` | `invalid`, `did_you_mean: typo@gmail.com` |
| `quota@sandbox.easyemailverification.com` | HTTP `402` (no credits) |

They make good integration tests: assert that `valid@` gives `accept`, `invalid@` gives `reject` and `typo@gmial.com` gives `suggest`. For production, create a free account, generate a key under [API settings](https://dashboard.easyemailverification.com/apisettings) and set `EEV_API_KEY`. Each verified address uses one credit; the free plan includes 50 verifications a day.

## Next steps

- [Email Verification API](https://www.easyemailverification.com/en-US/api): plans, limits and the other endpoints.
- [Result codes](https://www.easyemailverification.com/en-US/help/result-codes): every `result`, `reason` and risk signal.
- [Examples in other languages](https://github.com/EasyEmailVerification/api-examples) on GitHub.
