# Signup Form Examples for Each SDK

URL: https://email-validation.org/docs/sdks/examples/
Updated: 2026-11-06

> Documentation index: https://email-validation.org/llms.txt

One small web app per SDK language: a signup form that checks the email address with the official SDK before it
creates the account. Each one is one source file on the language's standard web stack, with its few dependencies
pinned, and each passes the same offline test against a stub of the API.

## What every example does

`GET /` shows a form with one email field. `POST /signup` validates the address and answers:

| Result | Answer |
|---|---|
| `valid`, `catch_all`, `unknown` | Accepted: "Thanks for signing up!" |
| `invalid` | Refused, with the reason, and the suggestion when `did_you_mean` is set |
| `disposable` | Refused ("Please use an address you will keep"); set `BLOCK_DISPOSABLE=false` to accept it |
| No answer in time, rate limited, out of credits or any other API error | Accepted: a check never stops a real person from signing up |

The app asks the service to answer within 6 seconds and gives up after 10 (`EV_TIMEOUT_MS`), without retries. It
logs each result's status and reason, never the address, and keeps your key on the server. Settings come from the
environment: `EV_API_KEY` (required), `EV_BASE_URL` (optional, for example `https://api-eu.email-validation.org` for
[EU data residency](https://email-validation.org/docs/eu-data-residency/)) and `PORT` (default 3000). Where to go from here, such as re-checking `unknown`
addresses later, is in the [signup form guide](https://email-validation.org/docs/guides/signup-form-validation/).

## Try it for free

Sign up with the [test addresses](https://email-validation.org/docs/test-addresses/): they cost nothing and return a fixed answer, so you can see
every branch. `accepted@sandbox.email-validation.org` is let in, `no_mailbox@sandbox.email-validation.org` is refused,
`likely_typo@sandbox.email-validation.org` gets a suggestion and `disposable@sandbox.email-validation.org` is refused as disposable.

## JavaScript and TypeScript

Node.js 24 or later, only `node:http` and the [JavaScript SDK](https://email-validation.org/docs/sdks/javascript/). Save the two files in a new
folder, then:

```bash
npm install
export EV_API_KEY=your_api_key
npm start
```

Open `http://localhost:3000/`.

```json
{
  "name": "email-validation-signup-example",
  "version": "1.0.0",
  "private": true,
  "description": "A signup form that validates the email address with the email-validation.org JavaScript SDK.",
  "license": "MIT",
  "type": "module",
  "engines": {
    "node": ">=24"
  },
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "@email-validation/sdk": "1.1.0"
  }
}
```

```js
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key node server.js, then open http://localhost:3000/
import { createServer } from 'node:http';
import { EmailValidation, EmailValidationError } from '@email-validation/sdk';

const ev = new EmailValidation({
  apiKey: process.env.EV_API_KEY ?? '',
  ...(process.env.EV_BASE_URL ? { baseUrl: process.env.EV_BASE_URL } : {}), // e.g. the EU endpoint
  timeoutMs: Number(process.env.EV_TIMEOUT_MS ?? 10_000), // a few seconds above the 6 s the service may take
  maxRetries: 0, // a signup form does not wait for retries: it lets the person in instead
});
const blockDisposable = process.env.BLOCK_DISPOSABLE !== 'false';

/** The decision for one address: whether to create the account, and what to tell the person. */
async function check(email) {
  if (!email) return { ok: false, message: 'Please enter your email address.' };
  let result;
  try {
    result = await ev.validate(email, { timeout: 6 }); // the service answers within 6 s
  } catch (err) {
    if (!(err instanceof EmailValidationError)) throw err;
    // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
    console.warn(`signup accepted without a check: ${err.code}`);
    return { ok: true, message: 'Thanks for signing up!' };
  }
  console.log(`signup check: ${result.status} (${result.reason})`); // never log the address itself
  if (result.status === 'invalid') {
    const hint = result.did_you_mean ? ` Did you mean ${result.did_you_mean}?` : '';
    return {
      ok: false,
      message: `We could not deliver to this address (${result.reason}). Please check it for typos.${hint}`,
    };
  }
  if (result.status === 'disposable' && blockDisposable) {
    return { ok: false, message: 'Please use an address you will keep, not a disposable one.' };
  }
  // valid, catch_all, unknown (and disposable when allowed): create the account here
  return { ok: true, message: 'Thanks for signing up!' };
}

const escapeHtml = (s) =>
  s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);

const page = (message = '') => `<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
<body>
<h1>Sign up</h1>
${message ? `<p>${escapeHtml(message)}</p>` : ''}
<form method="post" action="/signup">
<label>Email <input type="email" name="email" required></label>
<button type="submit">Sign up</button>
</form>
</body>
</html>
`;

const send = (res, status, html) => {
  res.writeHead(status, { 'content-type': 'text/html; charset=utf-8' });
  res.end(html);
};

const server = createServer(async (req, res) => {
  try {
    if (req.method === 'GET' && req.url === '/') return send(res, 200, page());
    if (req.method !== 'POST' || req.url !== '/signup') return send(res, 404, page('Not found.'));
    let body = '';
    for await (const chunk of req) {
      body += chunk;
      if (body.length > 10_000) return send(res, 413, page('That is too long.'));
    }
    const email = (new URLSearchParams(body).get('email') ?? '').trim();
    const { ok, message } = await check(email);
    send(res, ok ? 200 : 422, page(message));
  } catch (err) {
    console.error('signup failed:', err instanceof Error ? err.name : 'error');
    send(res, 500, page('Something went wrong. Please try again.'));
  }
});

const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => console.log(`Listening on http://localhost:${port}/`));
```

## Python

Python 3.11 or later, only the standard library's `http.server` and the [Python SDK](https://email-validation.org/docs/sdks/python/). The
requirements file pins the SDK and its dependencies with their hashes. Save the two files in a new folder, then:

```bash
python3 -m venv .venv && . .venv/bin/activate
pip install --require-hashes -r requirements.txt
EV_API_KEY=your_api_key python app.py
```

Open `http://localhost:3000/`.

```text
# The example's dependencies, exact versions with each wheel's sha256: pip install --require-hashes -r requirements.txt
# The SDK and the closure of its one dependency, httpx, for CPython 3.11 and later.
email-validation-sdk==1.1.0 --hash=sha256:fed9456b25af76b3f6e66a971900435c01bc2bec9f9269880302e5093a2d03b2
anyio==4.15.1 --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101
certifi==2026.10.23 --hash=sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1
h11==0.16.0 --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
httpcore==1.0.9 --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55
httpx==0.28.1 --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
idna==3.20 --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
typing_extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8
```

```python
"""A signup form that checks the email address with the email-validation.org SDK before it creates the account.

Run: EV_API_KEY=your_key python app.py, then open http://localhost:3000/
"""

import html
import logging
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs

from email_validation import EmailValidation, EmailValidationError

logging.basicConfig(level=logging.INFO, format="%(message)s")
log = logging.getLogger("signup")

ev = EmailValidation(
    os.environ.get("EV_API_KEY", ""),
    base_url=os.environ.get("EV_BASE_URL") or None,  # e.g. the EU endpoint
    timeout=int(os.environ.get("EV_TIMEOUT_MS", "10000")) / 1000,  # a few seconds above the 6 s the service may take
    max_retries=0,  # a signup form does not wait for retries: it lets the person in instead
)
BLOCK_DISPOSABLE = os.environ.get("BLOCK_DISPOSABLE") != "false"


def check(email: str) -> tuple[bool, str]:
    """The decision for one address: whether to create the account, and what to tell the person."""
    if not email:
        return False, "Please enter your email address."
    try:
        result = ev.validate(email, timeout=6)  # the service answers within 6 s
    except EmailValidationError as err:
        # no answer, rate limited, out of credits ...: never block a real person because of it (fail open)
        log.warning("signup accepted without a check: %s", err.code)
        return True, "Thanks for signing up!"
    log.info("signup check: %s (%s)", result["status"], result["reason"])  # never log the address itself
    if result["status"] == "invalid":
        hint = f" Did you mean {result['did_you_mean']}?" if result["did_you_mean"] else ""
        return False, f"We could not deliver to this address ({result['reason']}). Please check it for typos.{hint}"
    if result["status"] == "disposable" and BLOCK_DISPOSABLE:
        return False, "Please use an address you will keep, not a disposable one."
    # valid, catch_all, unknown (and disposable when allowed): create the account here
    return True, "Thanks for signing up!"


def page(message: str = "") -> bytes:
    note = f"<p>{html.escape(message)}</p>\n" if message else ""
    return f"""<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
<body>
<h1>Sign up</h1>
{note}<form method="post" action="/signup">
<label>Email <input type="email" name="email" required></label>
<button type="submit">Sign up</button>
</form>
</body>
</html>
""".encode()


class Handler(BaseHTTPRequestHandler):
    def send(self, status: int, body: bytes) -> None:
        self.send_response(status)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self) -> None:
        self.send(200, page()) if self.path == "/" else self.send(404, page("Not found."))

    def do_POST(self) -> None:
        if self.path != "/signup":
            return self.send(404, page("Not found."))
        length = int(self.headers.get("Content-Length") or 0)
        if length > 10_000:
            return self.send(413, page("That is too long."))
        form = parse_qs(self.rfile.read(length).decode("utf-8", "replace"))
        ok, message = check(form.get("email", [""])[0].strip())
        self.send(200 if ok else 422, page(message))


if __name__ == "__main__":
    port = int(os.environ.get("PORT", "3000"))
    server = ThreadingHTTPServer(("", port), Handler)
    log.info("Listening on http://localhost:%d/", port)
    server.serve_forever()
```

## Go

Go 1.22 or later, only `net/http` and the [Go SDK](https://email-validation.org/docs/sdks/go/). Save the two files in a new folder, then:

```bash
go mod tidy
export EV_API_KEY=your_api_key
go run .
```

Open `http://localhost:3000/`.

```text
module signup

go 1.22

require email-validation.org/go/emailvalidation v1.1.0
```

```go
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key go run ., then open http://localhost:3000/
package main

import (
	"context"
	"errors"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"email-validation.org/go/emailvalidation"
)

var page = template.Must(template.New("page").Parse(`<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
<body>
<h1>Sign up</h1>
{{if .}}<p>{{.}}</p>
{{end}}<form method="post" action="/signup">
<label>Email <input type="email" name="email" required></label>
<button type="submit">Sign up</button>
</form>
</body>
</html>
`))

type app struct {
	ev              *emailvalidation.Client
	blockDisposable bool
}

// check decides for one address: whether to create the account, and what to tell the person.
func (a *app) check(ctx context.Context, email string) (bool, string) {
	if email == "" {
		return false, "Please enter your email address."
	}
	// the service answers within 6 s
	result, err := a.ev.Validate(ctx, email, &emailvalidation.ValidateOptions{TimeoutSeconds: 6})
	if err != nil {
		// no answer, rate limited, out of credits …: never block a real person because of it (fail open)
		var apiErr *emailvalidation.Error
		if errors.As(err, &apiErr) {
			log.Printf("signup accepted without a check: %s", apiErr.Code)
		}
		return true, "Thanks for signing up!"
	}
	log.Printf("signup check: %s (%s)", result.Status, result.Reason) // never log the address itself
	switch {
	case result.Status == emailvalidation.StatusInvalid:
		hint := ""
		if result.DidYouMean != nil {
			hint = fmt.Sprintf(" Did you mean %s?", *result.DidYouMean)
		}
		return false, fmt.Sprintf("We could not deliver to this address (%s). Please check it for typos.%s", result.Reason, hint)
	case result.Status == emailvalidation.StatusDisposable && a.blockDisposable:
		return false, "Please use an address you will keep, not a disposable one."
	}
	// valid, catch_all, unknown (and disposable when allowed): create the account here
	return true, "Thanks for signing up!"
}

func (a *app) signup(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 10_000)
	ok, message := a.check(r.Context(), strings.TrimSpace(r.PostFormValue("email")))
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if !ok {
		w.WriteHeader(http.StatusUnprocessableEntity)
	}
	_ = page.Execute(w, message)
}

func main() {
	timeoutMs, err := strconv.Atoi(env("EV_TIMEOUT_MS", "10000")) // a few seconds above the 6 s the service may take
	if err != nil {
		log.Fatal("EV_TIMEOUT_MS: not a number")
	}
	opts := []emailvalidation.Option{
		emailvalidation.WithTimeout(time.Duration(timeoutMs) * time.Millisecond),
		emailvalidation.WithMaxRetries(0), // a signup form does not wait for retries: it lets the person in instead
	}
	if base := os.Getenv("EV_BASE_URL"); base != "" {
		opts = append(opts, emailvalidation.WithBaseURL(base)) // e.g. the EU endpoint
	}
	ev, err := emailvalidation.New(os.Getenv("EV_API_KEY"), opts...)
	if err != nil {
		log.Fatal(err)
	}
	a := &app{ev: ev, blockDisposable: os.Getenv("BLOCK_DISPOSABLE") != "false"}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = page.Execute(w, "")
	})
	mux.HandleFunc("POST /signup", a.signup)
	port := env("PORT", "3000")
	log.Printf("Listening on http://localhost:%s/", port)
	log.Fatal(http.ListenAndServe(":"+port, mux))
}

func env(name, fallback string) string {
	if v := os.Getenv(name); v != "" {
		return v
	}
	return fallback
}
```

## PHP

PHP 8.1 or later with ext-curl, PHP's built-in web server and the [PHP SDK](https://email-validation.org/docs/sdks/php/) from Composer. Save the
two files in a new folder, then:

```bash
composer install
export EV_API_KEY=your_api_key
php -S localhost:3000 index.php
```

Open `http://localhost:3000/`. The built-in server is for development: in production, serve `index.php` with your
usual web server.

```json
{
  "name": "email-validation/signup-example",
  "description": "A signup form that validates the email address with the email-validation.org PHP SDK.",
  "type": "project",
  "license": "MIT",
  "require": {
    "php": ">=8.1",
    "email-validation/sdk": "1.1.0"
  },
  "config": {
    "sort-packages": true
  }
}
```

```php
<?php

// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key php -S localhost:3000 index.php, then open http://localhost:3000/

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use EmailValidation\Client;
use EmailValidation\Exception\EmailValidationException;

/** The decision for one address: whether to create the account, and what to tell the person. */
function check(string $email): array
{
    if ($email === '') {
        return [false, 'Please enter your email address.'];
    }
    $baseUrl = getenv('EV_BASE_URL');
    $client = new Client(
        apiKey: (string) getenv('EV_API_KEY'),
        baseUrl: $baseUrl !== false && $baseUrl !== '' ? $baseUrl : null, // e.g. the EU endpoint
        timeout: (int) (getenv('EV_TIMEOUT_MS') ?: 10000) / 1000, // a few seconds above the 6 s the service may take
        maxRetries: 0, // a signup form does not wait for retries: it lets the person in instead
    );
    try {
        $result = $client->validate($email, timeout: 6); // the service answers within 6 s
    } catch (EmailValidationException $e) {
        // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
        error_log('signup accepted without a check: ' . $e->errorCode);
        return [true, 'Thanks for signing up!'];
    }
    error_log("signup check: {$result->status} ({$result->reason})"); // never log the address itself
    if ($result->status === 'invalid') {
        $hint = $result->didYouMean !== null ? " Did you mean {$result->didYouMean}?" : '';
        return [false, "We could not deliver to this address ({$result->reason}). Please check it for typos.{$hint}"];
    }
    if ($result->status === 'disposable' && getenv('BLOCK_DISPOSABLE') !== 'false') {
        return [false, 'Please use an address you will keep, not a disposable one.'];
    }
    // valid, catch_all, unknown (and disposable when allowed): create the account here
    return [true, 'Thanks for signing up!'];
}

function page(string $message = ''): string
{
    $note = $message !== '' ? '<p>' . htmlspecialchars($message) . "</p>\n" : '';
    return <<<HTML
    <!doctype html>
    <html lang="en">
    <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
    <body>
    <h1>Sign up</h1>
    {$note}<form method="post" action="/signup">
    <label>Email <input type="email" name="email" required></label>
    <button type="submit">Sign up</button>
    </form>
    </body>
    </html>

    HTML;
}

header('Content-Type: text/html; charset=utf-8');
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $path === '/') {
    echo page();
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && $path === '/signup') {
    [$ok, $message] = check(trim((string) ($_POST['email'] ?? '')));
    http_response_code($ok ? 200 : 422);
    echo page($message);
} else {
    http_response_code(404);
    echo page('Not found.');
}
```

## Ruby

Ruby 3.2 or later, WEBrick and the [Ruby SDK](https://email-validation.org/docs/sdks/ruby/) from Bundler. Save the two files in a new folder,
then:

```bash
bundle install
export EV_API_KEY=your_api_key
bundle exec ruby app.rb
```

Open `http://localhost:3000/`.

```ruby
# frozen_string_literal: true

source "https://rubygems.org"

ruby ">= 3.2"

gem "email-validation-sdk", "1.1.0"
gem "webrick", "1.9.2"
```

```ruby
# A signup form that checks the email address with the email-validation.org SDK before it creates the account.
# Run: EV_API_KEY=your_key bundle exec ruby app.rb, then open http://localhost:3000/
require "cgi"
require "email_validation_sdk"
require "webrick"

$stdout.sync = true

EV = EmailValidation::Client.new(
  api_key: ENV.fetch("EV_API_KEY", ""),
  base_url: ENV["EV_BASE_URL"].to_s.empty? ? nil : ENV["EV_BASE_URL"], # e.g. the EU endpoint
  timeout: Integer(ENV.fetch("EV_TIMEOUT_MS", "10000")) / 1000.0, # a few seconds above the 6 s the service may take
  max_retries: 0 # a signup form does not wait for retries: it lets the person in instead
)
BLOCK_DISPOSABLE = ENV["BLOCK_DISPOSABLE"] != "false"

# The decision for one address: whether to create the account, and what to tell the person.
def check(email)
  return [false, "Please enter your email address."] if email.empty?

  begin
    result = EV.validate(email, timeout: 6) # the service answers within 6 s
  rescue EmailValidation::Error => e
    # no answer, rate limited, out of credits …: never block a real person because of it (fail open)
    puts "signup accepted without a check: #{e.code}"
    return [true, "Thanks for signing up!"]
  end
  puts "signup check: #{result.status} (#{result.reason})" # never log the address itself
  if result.status == "invalid"
    hint = result.did_you_mean ? " Did you mean #{result.did_you_mean}?" : ""
    return [false, "We could not deliver to this address (#{result.reason}). Please check it for typos.#{hint}"]
  end
  if result.status == "disposable" && BLOCK_DISPOSABLE
    return [false, "Please use an address you will keep, not a disposable one."]
  end

  # valid, catch_all, unknown (and disposable when allowed): create the account here
  [true, "Thanks for signing up!"]
end

def page(message = "")
  note = message.empty? ? "" : "<p>#{CGI.escapeHTML(message)}</p>\n"
  <<~HTML
    <!doctype html>
    <html lang="en">
    <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
    <body>
    <h1>Sign up</h1>
    #{note}<form method="post" action="/signup">
    <label>Email <input type="email" name="email" required></label>
    <button type="submit">Sign up</button>
    </form>
    </body>
    </html>
  HTML
end

port = Integer(ENV.fetch("PORT", "3000"))
server = WEBrick::HTTPServer.new(Port: port, AccessLog: [], Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN))
server.mount_proc "/" do |req, res|
  res.content_type = "text/html; charset=utf-8"
  if req.request_method == "GET" && req.path == "/"
    res.body = page
  elsif req.request_method == "POST" && req.path == "/signup"
    ok, message = check(req.query["email"].to_s.strip)
    res.status = ok ? 200 : 422
    res.body = page(message)
  else
    res.status = 404
    res.body = page("Not found.")
  end
end
trap("INT") { server.shutdown }
trap("TERM") { server.shutdown }
puts "Listening on http://localhost:#{port}/"
server.start
```

## Java

Available once the [Java SDK](https://email-validation.org/docs/sdks/java/) is on Maven Central: it is not published yet, so `mvn` cannot fetch
it today. Java 17 or later and Maven, only the JDK's `com.sun.net.httpserver` and the SDK. Save `pom.xml` in a new
folder and `SignupApp.java` under `src/main/java/example/`, then:

```bash
export EV_API_KEY=your_api_key
mvn -q compile exec:java
```

Open `http://localhost:3000/`.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>example</groupId>
  <artifactId>signup</artifactId>
  <version>1.0.0</version>
  <description>A signup form that validates the email address with the email-validation.org Java SDK.</description>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>17</maven.compiler.release>
  </properties>

  <!-- Exact versions only, no ranges: Maven resolves this file the same way every time. -->
  <dependencies>
    <dependency>
      <groupId>org.email-validation</groupId>
      <artifactId>email-validation-sdk</artifactId>
      <version>1.1.0</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.16.0</version>
        <configuration>
          <compilerArgs>
            <arg>-Xlint:all</arg>
            <arg>-Werror</arg>
          </compilerArgs>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
      <!-- mvn compile exec:java starts the app -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.6.4</version>
        <configuration>
          <mainClass>example.SignupApp</mainClass>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
```

```java
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key mvn -q compile exec:java, then open http://localhost:3000/
package example;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.Executors;
import org.email_validation.sdk.EmailValidationClient;
import org.email_validation.sdk.EmailValidationException;
import org.email_validation.sdk.Status;
import org.email_validation.sdk.ValidateOptions;
import org.email_validation.sdk.ValidationResult;

public final class SignupApp {
  private static final EmailValidationClient EV = client();
  private static final boolean BLOCK_DISPOSABLE = !"false".equals(System.getenv("BLOCK_DISPOSABLE"));

  private static EmailValidationClient client() {
    EmailValidationClient.Builder builder =
        EmailValidationClient.builder(Objects.requireNonNullElse(System.getenv("EV_API_KEY"), ""))
            // a few seconds above the 6 s the service may take
            .timeout(Duration.ofMillis(Long.parseLong(env("EV_TIMEOUT_MS", "10000"))))
            .maxRetries(0); // a signup form does not wait for retries: it lets the person in instead
    String baseUrl = System.getenv("EV_BASE_URL");
    if (baseUrl != null && !baseUrl.isEmpty()) builder.baseUrl(baseUrl); // e.g. the EU endpoint
    return builder.build();
  }

  /** The decision for one address: whether to create the account, and what to tell the person. */
  record Decision(boolean ok, String message) {}

  static Decision check(String email) {
    if (email.isEmpty()) return new Decision(false, "Please enter your email address.");
    ValidationResult result;
    try {
      // the service answers within 6 s
      result = EV.validate(email, ValidateOptions.builder().timeoutSeconds(6).build());
    } catch (EmailValidationException e) {
      // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
      System.err.println("signup accepted without a check: " + e.code());
      return new Decision(true, "Thanks for signing up!");
    }
    System.out.println("signup check: " + result.status() + " (" + result.reason() + ")"); // never the address
    if (Status.INVALID.equals(result.status())) {
      String hint = result.didYouMean() != null ? " Did you mean " + result.didYouMean() + "?" : "";
      return new Decision(
          false,
          "We could not deliver to this address (" + result.reason() + "). Please check it for typos." + hint);
    }
    if (Status.DISPOSABLE.equals(result.status()) && BLOCK_DISPOSABLE) {
      return new Decision(false, "Please use an address you will keep, not a disposable one.");
    }
    // valid, catch_all, unknown (and disposable when allowed): create the account here
    return new Decision(true, "Thanks for signing up!");
  }

  static String escapeHtml(String s) {
    return s.replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace("\"", "&quot;")
        .replace("'", "&#39;");
  }

  static String page(String message) {
    return """
        <!doctype html>
        <html lang="en">
        <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
        <body>
        <h1>Sign up</h1>
        %s<form method="post" action="/signup">
        <label>Email <input type="email" name="email" required></label>
        <button type="submit">Sign up</button>
        </form>
        </body>
        </html>
        """
        .formatted(message.isEmpty() ? "" : "<p>" + escapeHtml(message) + "</p>\n");
  }

  static void send(HttpExchange exchange, int status, String html) throws IOException {
    byte[] body = html.getBytes(StandardCharsets.UTF_8);
    exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
    exchange.sendResponseHeaders(status, body.length);
    try (var out = exchange.getResponseBody()) {
      out.write(body);
    }
  }

  /** The email field of a form post (application/x-www-form-urlencoded). */
  static String emailField(String body) {
    for (String pair : body.split("&")) {
      int eq = pair.indexOf('=');
      String name = URLDecoder.decode(eq < 0 ? pair : pair.substring(0, eq), StandardCharsets.UTF_8);
      if (name.equals("email")) {
        return eq < 0 ? "" : URLDecoder.decode(pair.substring(eq + 1), StandardCharsets.UTF_8).trim();
      }
    }
    return "";
  }

  static void handle(HttpExchange exchange) throws IOException {
    try (exchange) {
      String method = exchange.getRequestMethod();
      String path = exchange.getRequestURI().getPath();
      if (method.equals("GET") && path.equals("/")) {
        send(exchange, 200, page(""));
      } else if (method.equals("POST") && path.equals("/signup")) {
        byte[] body = exchange.getRequestBody().readNBytes(10_001);
        if (body.length > 10_000) {
          send(exchange, 413, page("That is too long."));
          return;
        }
        Decision d;
        try {
          d = check(emailField(new String(body, StandardCharsets.UTF_8)));
        } catch (RuntimeException e) {
          System.err.println("signup failed: " + e.getClass().getSimpleName());
          send(exchange, 500, page("Something went wrong. Please try again."));
          return;
        }
        send(exchange, d.ok() ? 200 : 422, page(d.message()));
      } else {
        send(exchange, 404, page("Not found."));
      }
    }
  }

  private static String env(String name, String fallback) {
    String value = System.getenv(name);
    return value == null || value.isEmpty() ? fallback : value;
  }

  public static void main(String[] args) throws IOException {
    int port = Integer.parseInt(env("PORT", "3000"));
    HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
    server.createContext("/", SignupApp::handle);
    server.setExecutor(Executors.newCachedThreadPool());
    server.start();
    System.out.println("Listening on http://localhost:" + port + "/");
  }
}
```

## C#

.NET 10, an ASP.NET Core minimal API and the [C# SDK](https://email-validation.org/docs/sdks/csharp/) from NuGet. Save the two files in a new
folder, then:

```bash
dotnet restore
export EV_API_KEY=your_api_key
dotnet run
```

Open `http://localhost:3000/`.

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <!-- packages.lock.json pins every package; a locked-mode restore installs exactly those -->
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="EmailValidation.Sdk" Version="1.1.0" />
  </ItemGroup>

</Project>
```

```csharp
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key dotnet run, then open http://localhost:3000/
using System.Net;
using EmailValidation;

var options = new EmailValidationClientOptions
{
    // a few seconds above the 6 s the service may take
    Timeout = TimeSpan.FromMilliseconds(int.Parse(Environment.GetEnvironmentVariable("EV_TIMEOUT_MS") ?? "10000")),
    MaxRetries = 0, // a signup form does not wait for retries: it lets the person in instead
};
var baseUrl = Environment.GetEnvironmentVariable("EV_BASE_URL");
if (!string.IsNullOrEmpty(baseUrl)) options.BaseUrl = baseUrl; // e.g. the EU endpoint
using var ev = new EmailValidationClient(Environment.GetEnvironmentVariable("EV_API_KEY") ?? "", options);
var blockDisposable = Environment.GetEnvironmentVariable("BLOCK_DISPOSABLE") != "false";

var app = WebApplication.CreateBuilder(args).Build();

app.MapGet("/", () => Page(200, ""));

app.MapPost("/signup", async (HttpRequest request) =>
{
    var form = await request.ReadFormAsync();
    var email = form["email"].ToString().Trim();
    var (ok, message) = await Check(email);
    return Page(ok ? 200 : 422, message);
});

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.Run($"http://0.0.0.0:{port}");

// The decision for one address: whether to create the account, and what to tell the person.
async Task<(bool Ok, string Message)> Check(string email)
{
    if (email.Length == 0) return (false, "Please enter your email address.");
    ValidationResult result;
    try
    {
        result = await ev.ValidateAsync(email, new ValidateOptions { TimeoutSeconds = 6 }); // the service answers within 6 s
    }
    catch (EmailValidationException e)
    {
        // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
        Console.Error.WriteLine($"signup accepted without a check: {e.Code}");
        return (true, "Thanks for signing up!");
    }
    Console.WriteLine($"signup check: {result.Status} ({result.Reason})"); // never log the address itself
    if (result.Status == Status.Invalid)
    {
        var hint = result.DidYouMean is { } suggestion ? $" Did you mean {suggestion}?" : "";
        return (false, $"We could not deliver to this address ({result.Reason}). Please check it for typos.{hint}");
    }
    if (result.Status == Status.Disposable && blockDisposable)
        return (false, "Please use an address you will keep, not a disposable one.");
    // valid, catch_all, unknown (and disposable when allowed): create the account here
    return (true, "Thanks for signing up!");
}

static IResult Page(int status, string message) => Results.Content($"""
    <!doctype html>
    <html lang="en">
    <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
    <body>
    <h1>Sign up</h1>
    {(message.Length > 0 ? $"<p>{WebUtility.HtmlEncode(message)}</p>\n" : "")}<form method="post" action="/signup">
    <label>Email <input type="email" name="email" required></label>
    <button type="submit">Sign up</button>
    </form>
    </body>
    </html>

    """, "text/html; charset=utf-8", statusCode: status);
```

## Rust

Rust 1.88 or later, [axum](https://crates.io/crates/axum) on Tokio and the [Rust SDK](https://email-validation.org/docs/sdks/rust/). Save
`Cargo.toml` in a new folder and `main.rs` under `src/`, then:

```bash
cargo build
export EV_API_KEY=your_api_key
cargo run
```

Open `http://localhost:3000/`.

```toml
[package]
name = "email-validation-signup-example"
version = "1.0.0"
edition = "2024"
rust-version = "1.88"
license = "MIT"
description = "A signup form that validates the email address with the email-validation.org Rust SDK."
publish = false

[dependencies]
email-validation-sdk = "1.1"
axum = { version = "0.8", default-features = false, features = ["form", "http1", "tokio"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
```

```rust
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key cargo run, then open http://localhost:3000/
use std::{env, time::Duration};

use axum::{
    Form, Router,
    extract::State,
    http::StatusCode,
    response::Html,
    routing::{get, post},
};
use email_validation::{Client, Status, ValidateOptions};
use serde::Deserialize;

#[derive(Clone)]
struct App {
    ev: Client,
    block_disposable: bool,
}

#[derive(Deserialize)]
struct Signup {
    #[serde(default)]
    email: String,
}

/// The decision for one address: whether to create the account, and what to tell the person.
async fn check(app: &App, email: &str) -> (bool, String) {
    if email.is_empty() {
        return (false, "Please enter your email address.".into());
    }
    // the service answers within 6 s
    let result = match app
        .ev
        .validate_with(email, ValidateOptions::new().timeout(6))
        .await
    {
        Ok(result) => result,
        Err(err) => {
            // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
            eprintln!("signup accepted without a check: {}", err.code());
            return (true, "Thanks for signing up!".into());
        }
    };
    println!("signup check: {} ({})", result.status, result.reason); // never log the address itself
    match result.status {
        Status::Invalid => {
            let hint = match &result.did_you_mean {
                Some(suggestion) => format!(" Did you mean {suggestion}?"),
                None => String::new(),
            };
            let reason = &result.reason;
            (
                false,
                format!(
                    "We could not deliver to this address ({reason}). Please check it for typos.{hint}"
                ),
            )
        }
        Status::Disposable if app.block_disposable => (
            false,
            "Please use an address you will keep, not a disposable one.".into(),
        ),
        // valid, catch_all, unknown (and disposable when allowed): create the account here
        _ => (true, "Thanks for signing up!".into()),
    }
}

fn escape_html(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

fn page(message: &str) -> Html<String> {
    let message = if message.is_empty() {
        String::new()
    } else {
        format!("<p>{}</p>\n", escape_html(message))
    };
    Html(format!(
        r#"<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
<body>
<h1>Sign up</h1>
{message}<form method="post" action="/signup">
<label>Email <input type="email" name="email" required></label>
<button type="submit">Sign up</button>
</form>
</body>
</html>
"#
    ))
}

async fn signup(State(app): State<App>, Form(form): Form<Signup>) -> (StatusCode, Html<String>) {
    let (ok, message) = check(&app, form.email.trim()).await;
    let status = if ok {
        StatusCode::OK
    } else {
        StatusCode::UNPROCESSABLE_ENTITY
    };
    (status, page(&message))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut builder = Client::builder(env::var("EV_API_KEY").unwrap_or_default())
        // a few seconds above the 6 s the service may take
        .timeout(Duration::from_millis(
            env::var("EV_TIMEOUT_MS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(10_000),
        ))
        .max_retries(0); // a signup form does not wait for retries: it lets the person in instead
    if let Ok(base_url) = env::var("EV_BASE_URL") {
        builder = builder.base_url(base_url); // e.g. the EU endpoint
    }
    let app = App {
        ev: builder.build()?,
        block_disposable: env::var("BLOCK_DISPOSABLE").map_or(true, |v| v != "false"),
    };
    let router = Router::new()
        .route("/", get(|| async { page("") }))
        .route("/signup", post(signup))
        .with_state(app);
    let port: u16 = env::var("PORT")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(3000);
    let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await?;
    println!("Listening on http://localhost:{port}/");
    axum::serve(listener, router).await?;
    Ok(())
}
```

## Swift

Swift 6 on macOS 14 or later, [Hummingbird](https://github.com/hummingbird-project/hummingbird) 2 and the
[Swift SDK](https://email-validation.org/docs/sdks/swift/). Swift has no HTTP server in its standard library or in Foundation, so a web form
needs a server package; Hummingbird is a small one on SwiftNIO. Save `Package.swift` in a new folder and `main.swift`
under `Sources/Signup/`, then:

```bash
swift build
export EV_API_KEY=your_api_key
swift run Signup
```

Open `http://localhost:3000/`.

```swift
// swift-tools-version:6.0
// A signup form that validates the email address with the email-validation.org Swift SDK.
import PackageDescription

let package = Package(
    name: "Signup",
    platforms: [.macOS(.v14)],
    dependencies: [
        .package(url: "https://github.com/email-validation-dev/email-validation-swift.git", from: "1.1.0"),
        .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.28.0"),
    ],
    targets: [
        .executableTarget(
            name: "Signup",
            dependencies: [
                .product(name: "EmailValidation", package: "email-validation-swift"),
                .product(name: "Hummingbird", package: "hummingbird"),
            ]
        )
    ]
)
```

```swift
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key swift run, then open http://localhost:3000/
import EmailValidation
import Foundation
import Hummingbird

let env = ProcessInfo.processInfo.environment

/// The decision for one address: whether to create the account, and what to tell the person.
struct Signup: Sendable {
    let ev: EmailValidationClient
    let blockDisposable: Bool

    func check(_ email: String) async -> (ok: Bool, message: String) {
        if email.isEmpty { return (false, "Please enter your email address.") }
        let result: ValidationResult
        do {
            result = try await ev.validate(email, timeout: 6)  // the service answers within 6 s
        } catch {
            // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
            print("signup accepted without a check: \((error as? EmailValidationError)?.code.rawValue ?? "error")")
            return (true, "Thanks for signing up!")
        }
        print("signup check: \(result.status.rawValue) (\(result.reason.rawValue))")  // never log the address itself
        if result.status == .invalid {
            let hint = result.didYouMean.map { " Did you mean \($0)?" } ?? ""
            return (
                false,
                "We could not deliver to this address (\(result.reason.rawValue)). Please check it for typos.\(hint)"
            )
        }
        if result.status == .disposable && blockDisposable {
            return (false, "Please use an address you will keep, not a disposable one.")
        }
        // valid, catch_all, unknown (and disposable when allowed): create the account here
        return (true, "Thanks for signing up!")
    }
}

func escapeHTML(_ s: String) -> String {
    s.replacingOccurrences(of: "&", with: "&amp;").replacingOccurrences(of: "<", with: "&lt;")
        .replacingOccurrences(of: ">", with: "&gt;").replacingOccurrences(of: "\"", with: "&quot;")
        .replacingOccurrences(of: "'", with: "&#39;")
}

func page(_ status: HTTPResponse.Status, _ message: String = "") -> Response {
    let html = """
        <!doctype html>
        <html lang="en">
        <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
        <body>
        <h1>Sign up</h1>
        \(message.isEmpty ? "" : "<p>\(escapeHTML(message))</p>\n")<form method="post" action="/signup">
        <label>Email <input type="email" name="email" required></label>
        <button type="submit">Sign up</button>
        </form>
        </body>
        </html>

        """
    return Response(
        status: status,
        headers: [.contentType: "text/html; charset=utf-8"],
        body: .init(byteBuffer: ByteBuffer(string: html))
    )
}

struct SignupForm: Decodable {
    var email: String?
}

let signup = Signup(
    ev: try EmailValidationClient(
        apiKey: env["EV_API_KEY"] ?? "",
        // EV_BASE_URL: e.g. the EU endpoint
        baseURL: env["EV_BASE_URL"].flatMap { $0.isEmpty ? nil : $0 } ?? Vocabulary.defaultBaseURL,
        // a few seconds above the 6 s the service may take
        timeout: (Double(env["EV_TIMEOUT_MS"] ?? "") ?? 10_000) / 1000,
        maxRetries: 0  // a signup form does not wait for retries: it lets the person in instead
    ),
    blockDisposable: env["BLOCK_DISPOSABLE"] != "false"
)

let router = Router()
router.get("/") { _, _ in page(.ok) }
router.post("/signup") { request, _ -> Response in
    let body = try await request.body.collect(upTo: 10_000)
    let form = (try? URLEncodedFormDecoder().decode(SignupForm.self, from: String(buffer: body))) ?? SignupForm()
    let email = (form.email ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
    let (ok, message) = await signup.check(email)
    return page(ok ? .ok : .unprocessableContent, message)
}

let port = Int(env["PORT"] ?? "") ?? 3000
let app = Application(router: router, configuration: .init(address: .hostname("0.0.0.0", port: port)))
print("Listening on http://localhost:\(port)/")
try await app.runService()
```

## Kotlin

Available once the [Kotlin SDK](https://email-validation.org/docs/sdks/kotlin/) is on Maven Central: it is not published yet, so Gradle cannot
fetch it today. JDK 17 or later and Gradle 9, only the JDK's `com.sun.net.httpserver` and the SDK (its calls are
`suspend` functions, so each request runs one in `runBlocking`). Save `build.gradle.kts` and an empty
`settings.gradle.kts` in a new folder and `Main.kt` under `src/main/kotlin/`, then:

```bash
gradle classes
export EV_API_KEY=your_api_key
gradle run
```

Open `http://localhost:3000/`.

```kotlin
plugins {
    kotlin("jvm") version "2.4.10"
    application
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.email-validation:email-validation-sdk-kotlin:1.1.0")
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
        allWarningsAsErrors.set(true)
    }
}

application {
    mainClass.set("MainKt")
}

// gradle.lockfile pins every dependency: a build never picks a version this file does not name
dependencyLocking {
    lockAllConfigurations()
}
```

```kotlin
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key gradle run, then open http://localhost:3000/
import com.sun.net.httpserver.HttpExchange
import com.sun.net.httpserver.HttpServer
import java.net.InetSocketAddress
import java.net.URLDecoder
import java.util.concurrent.Executors
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.runBlocking
import org.email_validation.kotlin.EmailValidationClient
import org.email_validation.kotlin.EmailValidationException
import org.email_validation.kotlin.Status

val ev = EmailValidationClient(System.getenv("EV_API_KEY") ?: "") {
    System.getenv("EV_BASE_URL")?.takeIf { it.isNotEmpty() }?.let { baseUrl = it } // e.g. the EU endpoint
    // a few seconds above the 6 s the service may take
    timeout = (System.getenv("EV_TIMEOUT_MS")?.toLongOrNull() ?: 10_000).milliseconds
    maxRetries = 0 // a signup form does not wait for retries: it lets the person in instead
}
val blockDisposable = System.getenv("BLOCK_DISPOSABLE") != "false"

/** The decision for one address: whether to create the account, and what to tell the person. */
suspend fun check(email: String): Pair<Boolean, String> {
    if (email.isEmpty()) return false to "Please enter your email address."
    val result = try {
        ev.validate(email, timeout = 6) // the service answers within 6 s
    } catch (e: EmailValidationException) {
        // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
        System.err.println("signup accepted without a check: ${e.code}")
        return true to "Thanks for signing up!"
    }
    println("signup check: ${result.status} (${result.reason})") // never log the address itself
    if (result.status == Status.INVALID) {
        val hint = result.didYouMean?.let { " Did you mean $it?" } ?: ""
        return false to "We could not deliver to this address (${result.reason}). Please check it for typos.$hint"
    }
    if (result.status == Status.DISPOSABLE && blockDisposable) {
        return false to "Please use an address you will keep, not a disposable one."
    }
    // valid, catch_all, unknown (and disposable when allowed): create the account here
    return true to "Thanks for signing up!"
}

fun escapeHtml(s: String): String =
    s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;")

fun page(message: String = ""): String = """
    |<!doctype html>
    |<html lang="en">
    |<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
    |<body>
    |<h1>Sign up</h1>
    |${if (message.isEmpty()) "" else "<p>${escapeHtml(message)}</p>\n"}<form method="post" action="/signup">
    |<label>Email <input type="email" name="email" required></label>
    |<button type="submit">Sign up</button>
    |</form>
    |</body>
    |</html>
    |""".trimMargin()

fun send(exchange: HttpExchange, status: Int, html: String) {
    val body = html.toByteArray()
    exchange.responseHeaders.set("Content-Type", "text/html; charset=utf-8")
    exchange.sendResponseHeaders(status, body.size.toLong())
    exchange.responseBody.use { it.write(body) }
}

/** The email field of a form post (application/x-www-form-urlencoded). */
fun emailField(body: String): String = body.split("&").firstNotNullOfOrNull { pair ->
    val (name, value) = pair.split("=", limit = 2).map { URLDecoder.decode(it, Charsets.UTF_8) } + ""
    if (name == "email") value.trim() else null
} ?: ""

fun handle(exchange: HttpExchange) = exchange.use {
    val path = exchange.requestURI.path
    when {
        exchange.requestMethod == "GET" && path == "/" -> send(exchange, 200, page())
        exchange.requestMethod == "POST" && path == "/signup" -> {
            val body = exchange.requestBody.readNBytes(10_001)
            if (body.size > 10_000) return@use send(exchange, 413, page("That is too long."))
            val (ok, message) = try {
                runBlocking { check(emailField(body.decodeToString())) }
            } catch (e: RuntimeException) {
                System.err.println("signup failed: ${e.javaClass.simpleName}")
                return@use send(exchange, 500, page("Something went wrong. Please try again."))
            }
            send(exchange, if (ok) 200 else 422, page(message))
        }
        else -> send(exchange, 404, page("Not found."))
    }
}

fun main() {
    val port = System.getenv("PORT")?.toIntOrNull() ?: 3000
    val server = HttpServer.create(InetSocketAddress(port), 0)
    server.createContext("/") { handle(it) }
    server.executor = Executors.newCachedThreadPool()
    server.start()
    println("Listening on http://localhost:$port/")
}
```
