diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8b500..8c1dcde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,30 @@ the library. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## 2026-08-17 + +### Changed +- Both Docker stacks (`compose.yaml`, `docker-compose-keycloak.yml`) now run + [Mailpit](https://mailpit.axllent.org/) instead of `docker-mailserver`. Mailpit captures outbound + mail and serves it as a web inbox on http://localhost:8025, so the demo exercises the real + registration-verification and password-reset flows: register, read the message in the browser, + click the link, log in. (Resend-verification is still broken for an unrelated reason; see the + known-limitation note in `docs/CONFIGURATION.md`.) +- Neither stack sets `USER_REGISTRATION_SENDVERIFICATIONEMAIL` any more; both use the base + `user.registration.sendVerificationEmail: true` +- App containers point at `SPRING_MAIL_HOST: mailpit` / `SPRING_MAIL_PORT: 1025`, auth and STARTTLS off + +### Fixed +- `page.title.registration-pending-verification` in `messages.properties` was spelled + `registration-ending-verification`, so the pending-verification page rendered + `??page.title.registration-pending-verification_en??` as its title. The page is on the Docker demo's + happy path now that verification is on, which is how the typo surfaced + +### Removed +- `mailserver.env`, the `./config/` mail bind mount, the `maildata`/`mailstate`/`maillogs` volumes, the + `NET_ADMIN`/`SYS_PTRACE` capabilities, and the published SMTP ports 25/587. The `SELINUX_LABEL` + variable went with the bind mount, so `docker compose` no longer warns that it is unset + ## 2026-08-15 ### Added diff --git a/README.md b/README.md index 3e416a4..e336042 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,11 @@ docker compose up --build ``` The app image is built from source inside Docker, so the first build takes several minutes. When it is up, -open http://localhost:8080 and register at http://localhost:8080/user/register.html. This stack sets -`USER_REGISTRATION_SENDVERIFICATIONEMAIL=false`, so accounts are enabled at registration and you can log in -immediately. Its `mailserver` container is a relay with no route to real inboxes, so nothing it accepts will -reach an actual mailbox. The stack runs under the `dev` profile and loads no sample events. +open http://localhost:8080 and register at http://localhost:8080/user/register.html. The stack captures every +mail the app sends in [Mailpit](https://mailpit.axllent.org/) instead of delivering it, so the real flows work +end to end: open http://localhost:8025, click the verification link, then log in. Password reset from +http://localhost:8080/user/forgot-password.html arrives in the same inbox. The stack runs under the `dev` +profile and loads no sample events. Stop it with Ctrl-C, then `docker compose down -v` to remove the containers and their data. diff --git a/compose.yaml b/compose.yaml index bfe2023..064fb7f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -21,38 +21,25 @@ services: timeout: 5s retries: 3 - mailserver: - image: docker.io/mailserver/docker-mailserver:latest + mailpit: + image: axllent/mailpit:v1.30.7 container_name: springuser-mail - hostname: mailserver - domainname: local - env_file: mailserver.env ports: - - "25:25" - - "587:587" - volumes: - - maildata:/var/mail - - mailstate:/var/mail-state - - maillogs:/var/log/mail - - ./config/:/tmp/docker-mailserver/${SELINUX_LABEL} + # Web inbox. Open http://localhost:8025 to read the verification and password-reset mail the + # app sends. SMTP (1025) is not published: only the app container needs to reach it. + - "8025:8025" environment: - PERMIT_DOCKER: connected-networks - ONE_DIR: 1 - DMS_DEBUG: 0 - SPOOF_PROTECTION: 0 - REPORT_RECIPIENT: 1 - ENABLE_SPAMASSASSIN: 0 - ENABLE_CLAMAV: 0 - ENABLE_FAIL2BAN: 1 - ENABLE_POSTGREY: 0 - SMTP_ONLY: 1 - cap_add: - - NET_ADMIN - - SYS_PTRACE + MP_MAX_MESSAGES: 5000 + # The app connects without auth (see SPRING_MAIL_* below). These two keep Mailpit accepting the + # message if you point a client at it that insists on AUTH over a plaintext connection. + MP_SMTP_AUTH_ACCEPT_ANY: 1 + MP_SMTP_AUTH_ALLOW_INSECURE: 1 healthcheck: - test: ["CMD", "nc", "-z", "localhost", "25"] - interval: 30s - timeout: 10s + # No curl in the image (Alpine + busybox, so wget/nc exist but curl doesn't). `mailpit readyz` + # is what the image bakes into its own default HEALTHCHECK; this override just makes it explicit. + test: ["CMD", "/mailpit", "readyz"] + interval: 10s + timeout: 5s retries: 5 myapp-main: @@ -64,7 +51,7 @@ services: depends_on: myapp-db: condition: service_healthy - mailserver: + mailpit: condition: service_healthy ports: - "8080:8080" @@ -73,16 +60,15 @@ services: SPRING_DATASOURCE_USERNAME: springuser SPRING_DATASOURCE_PASSWORD: springuser SPRING_PROFILES_ACTIVE: dev - SPRING_MAIL_HOST: mailserver - SPRING_MAIL_PORT: 25 + # Mailpit captures everything the app sends and shows it at http://localhost:8025. It speaks + # plain SMTP on 1025 with no auth and no STARTTLS, so verification and password-reset mail is + # readable without any credentials. Registration verification is left at its base default of + # true (application.yml:113): register, open the web inbox, click the link, log in. + SPRING_MAIL_HOST: mailpit + SPRING_MAIL_PORT: 1025 SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false" SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false" SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_REQUIRED: "false" - # The mailserver container is a relay with no route to real inboxes, so a verification link - # would never arrive. With this false the framework enables new accounts immediately and you - # can log in straight after registering. To exercise verification instead, set this to true - # and point the SPRING_MAIL_* values above at a real SMTP server. - USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false" healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/actuator/health"] interval: 30s @@ -90,7 +76,4 @@ services: retries: 5 volumes: - maildata: - mailstate: - maillogs: userdb: diff --git a/docker-compose-keycloak.yml b/docker-compose-keycloak.yml index 2ad34e4..59a20c2 100644 --- a/docker-compose-keycloak.yml +++ b/docker-compose-keycloak.yml @@ -20,38 +20,25 @@ services: timeout: 5s retries: 3 - mailserver: - image: docker.io/mailserver/docker-mailserver:latest + mailpit: + image: axllent/mailpit:v1.30.7 container_name: springuser-mail - hostname: mailserver - domainname: local - env_file: mailserver.env ports: - - "25:25" - - "587:587" - volumes: - - maildata:/var/mail - - mailstate:/var/mail-state - - maillogs:/var/log/mail - - ./config/:/tmp/docker-mailserver/${SELINUX_LABEL} + # Web inbox. Open http://localhost:8025 to read the verification and password-reset mail the + # app sends. SMTP (1025) is not published: only the app container needs to reach it. + - "8025:8025" environment: - PERMIT_DOCKER: connected-networks - ONE_DIR: 1 - DMS_DEBUG: 0 - SPOOF_PROTECTION: 0 - REPORT_RECIPIENT: 1 - ENABLE_SPAMASSASSIN: 0 - ENABLE_CLAMAV: 0 - ENABLE_FAIL2BAN: 1 - ENABLE_POSTGREY: 0 - SMTP_ONLY: 1 - cap_add: - - NET_ADMIN - - SYS_PTRACE + MP_MAX_MESSAGES: 5000 + # The app connects without auth (see SPRING_MAIL_* below). These two keep Mailpit accepting the + # message if you point a client at it that insists on AUTH over a plaintext connection. + MP_SMTP_AUTH_ACCEPT_ANY: 1 + MP_SMTP_AUTH_ALLOW_INSECURE: 1 healthcheck: - test: ["CMD", "nc", "-z", "localhost", "25"] - interval: 30s - timeout: 10s + # No curl in the image (Alpine + busybox, so wget/nc exist but curl doesn't). `mailpit readyz` + # is what the image bakes into its own default HEALTHCHECK; this override just makes it explicit. + test: ["CMD", "/mailpit", "readyz"] + interval: 10s + timeout: 5s retries: 5 myapp-main: @@ -63,7 +50,7 @@ services: depends_on: myapp-db: condition: service_healthy - mailserver: + mailpit: condition: service_healthy # Not strictly required to boot, but the demo is useless until Keycloak answers, and this makes # `up` report the stack as ready only when a Keycloak login can actually be attempted. @@ -77,16 +64,15 @@ services: SPRING_DATASOURCE_USERNAME: springuser SPRING_DATASOURCE_PASSWORD: springuser SPRING_PROFILES_ACTIVE: docker-keycloak - SPRING_MAIL_HOST: mailserver - SPRING_MAIL_PORT: 25 + # Mailpit captures everything the app sends and shows it at http://localhost:8025. It speaks + # plain SMTP on 1025 with no auth and no STARTTLS, so verification and password-reset mail is + # readable without any credentials. Registration verification is left at its base default of + # true (application.yml:113): register, open the web inbox, click the link, log in. + SPRING_MAIL_HOST: mailpit + SPRING_MAIL_PORT: 1025 SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false" SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false" SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_REQUIRED: "false" - # The mailserver container is a relay with no route to real inboxes, so a verification link - # would never arrive. With this false the framework enables new accounts immediately and you - # can log in straight after registering. To exercise verification instead, set this to true - # and point the SPRING_MAIL_* values above at a real SMTP server. - USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false" healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/actuator/health"] interval: 30s @@ -133,8 +119,5 @@ services: condition: service_healthy volumes: - maildata: - mailstate: - maillogs: userdb: keycloak: diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md index 5509a8d..cf5181a 100644 --- a/docs/AUTHENTICATION.md +++ b/docs/AUTHENTICATION.md @@ -18,16 +18,20 @@ failed logins and lockouts land in the audit log. [`register.js`](../src/main/resources/static/js/user/register.js)), which posts JSON to `POST /user/registration`, not form data. 2. What happens next depends on `user.registration.sendVerificationEmail`: - - `true` (base `application.yml:113`): the account is created disabled, a verification email is sent, - and the browser lands on `/user/registration-pending-verification.html`. The emailed link is + - `true` (base `application.yml:113`, and what both Docker stacks run): the account is created + disabled, a verification email is sent, and the browser lands on + `/user/registration-pending-verification.html`. The emailed link is `GET /user/registrationConfirm?token=...`, which enables the account. Lost it? Request another at `/user/request-new-verification-email.html` ([`resend-verification.js`](../src/main/resources/static/js/user/resend-verification.js) posts - `POST /user/resendRegistrationToken`). + `POST /user/resendRegistrationToken`). Under `docker compose up` the message is captured by Mailpit + rather than delivered: open and click the link there. The resend page itself + is currently broken against `ds-spring-user-framework:5.3.0`: the endpoint binds the full + registration `UserDto`, whose `firstName`/`lastName`/`password`/`matchingPassword` are `@NotBlank`, + so the email-only payload the page posts is rejected with HTTP 400 and no mail goes out. - `false`: the account is created enabled, the framework logs the user straight in, and the browser - lands on `/user/registration-complete.html`. `application-local.yml-example:131` sets it false and - the Docker demo stack sets `USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false"` (`compose.yaml:85`), - so neither documented run path needs a working SMTP server. + lands on `/user/registration-complete.html`. `application-local.yml-example:131` sets it false, so + the Gradle `local` run path needs no SMTP server at all. 3. Log in at `/user/login.html` ([`login.js`](../src/main/resources/static/js/user/login.js)). The form posts to `/user/login`; success redirects to `/index.html?messageKey=message.login.success`. Ten failed attempts lock the account for 30 minutes (`application.yml:146-147`). @@ -36,7 +40,9 @@ failed logins and lockouts land in the audit log. `/user/forgot-password-change.html`, which posts `POST /user/savePassword` ([`forgot-password.js`](../src/main/resources/static/js/user/forgot-password.js), [`reset-password.js`](../src/main/resources/static/js/user/reset-password.js)). Both reset steps need - real mail, unlike registration. To change a password you know, `/user/update-password.html` posts + working mail in every profile: the Docker stacks capture it in Mailpit at , + and `playwright-test` turns the email off and reads the token through the test API instead + (`application-playwright-test.yml:8`). To change a password you know, `/user/update-password.html` posts `POST /user/updatePassword`. ## Passkeys @@ -160,8 +166,8 @@ Open http://localhost:8080/user/login.html, click "Login with Keycloak", sign in First login creates a local account with provider `KEYCLOAK` and email `demo@example.com`. The realm is `demo`, not `master`, so every OIDC URL is `/realms/demo/...`; `admin` / `admin` is a master realm account and cannot sign in to the demo app. The app's own registration form still works in this stack, and -like the `compose.yaml` stack it sets `USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false"`, so an account -registered there is enabled immediately rather than waiting on mail the bundled relay cannot deliver. +like the `compose.yaml` stack it sends the verification email through the bundled Mailpit container: open +, click the link in the message, and log in with the form. The compose file sets `SPRING_PROFILES_ACTIVE: docker-keycloak`, and every value in [`application-docker-keycloak.yml`](../src/main/resources/application-docker-keycloak.yml) comes from an environment variable in [`keycloak.env`](../keycloak.env): `DS_SPRING_USER_KEYCLOAK_CLIENT_ID`, diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d9094b6..abb0c27 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -63,20 +63,19 @@ Recognized elsewhere (fall back to a demo default when unset): | `DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD` | Production datasource (`application-prd.yml:10-12`). | | `WEBAUTHN_RP_ID`, `WEBAUTHN_RP_NAME`, `WEBAUTHN_ALLOWED_ORIGINS` | WebAuthn relying-party identity in `prd` (`application-prd.yml:41-43`). | | `DS_SPRING_USER_KEYCLOAK_CLIENT_ID`, `_CLIENT_SECRET`, `_PROVIDER_AUTHORIZATION_URI`, `_PROVIDER_TOKEN_URI`, `_PROVIDER_USER_INFO_URI`, `_PROVIDER_JWK_SET_URI` | Keycloak OAuth2 client and provider endpoints for `docker-keycloak`, consumed in `application-docker-keycloak.yml:19-20,35,48-50`; supplied by [`keycloak.env`](../keycloak.env) when you run `docker-compose-keycloak.yml`. There is no `_PROVIDER_ISSUER_URI`: `issuer-uri` is deliberately left unset, see [`keycloak/README.md`](../keycloak/README.md). | -| `SELINUX_LABEL` | Suffix on the mailserver's bind-mounted config path in `compose.yaml`/`docker-compose-keycloak.yml`. Unset by default; Docker Compose prints a harmless warning about it. | Any framework property can also be set through Spring's relaxed binding (`SCREAMING_SNAKE_CASE` of the dotted key). The Docker demo stack (`compose.yaml`) does this for the app container: -`SPRING_DATASOURCE_URL`/`_USERNAME`/`_PASSWORD` (→ `spring.datasource.*`), `SPRING_PROFILES_ACTIVE`, -`SPRING_MAIL_HOST`/`_PORT` and the `SPRING_MAIL_PROPERTIES_MAIL_SMTP_*` keys (→ `spring.mail.*`), and -`USER_REGISTRATION_SENDVERIFICATIONEMAIL` (→ `user.registration.sendVerificationEmail`). The same -pattern works for any other key, e.g. `USER_SECURITY_BCRYPTSTRENGTH` for `user.security.bcryptStrength`. +`SPRING_DATASOURCE_URL`/`_USERNAME`/`_PASSWORD` (→ `spring.datasource.*`), `SPRING_PROFILES_ACTIVE`, and +`SPRING_MAIL_HOST`/`_PORT` plus the `SPRING_MAIL_PROPERTIES_MAIL_SMTP_*` keys (→ `spring.mail.*`). The same +pattern works for any other key, e.g. `USER_SECURITY_BCRYPTSTRENGTH` for `user.security.bcryptStrength` or +`USER_REGISTRATION_SENDVERIFICATIONEMAIL` for `user.registration.sendVerificationEmail`. ## Mail - `spring.mail.username`, `spring.mail.password`, `spring.mail.host`, `spring.mail.port` (`application.yml:2-6`) configure the SMTP transport used for verification, password-reset, and notification email. The base file's `host` is a placeholder SES endpoint; set real credentials in your profile. - `user.registration.sendVerificationEmail` (`application.yml:113`) controls whether a new account must click a verification link before it can log in. `false` enables the account immediately at registration. -- The Docker demo stack's `mailserver` service (`compose.yaml`) is a relay only: `SMTP_ONLY: 1` (`compose.yaml:48`) with no route to real inboxes. That stack sets `USER_REGISTRATION_SENDVERIFICATIONEMAIL: "false"` (`compose.yaml:85`) so registered accounts activate immediately instead of waiting on mail nothing will deliver. +- Both Docker stacks (`compose.yaml` and `docker-compose-keycloak.yml`) run a `mailpit` service ([Mailpit](https://mailpit.axllent.org/)) that captures outbound mail instead of delivering it, and serves it as a web inbox on . The app container reaches it over plain SMTP with `SPRING_MAIL_HOST: mailpit` and `SPRING_MAIL_PORT: 1025`, auth and STARTTLS off. Neither stack overrides `user.registration.sendVerificationEmail`, so registration verification and password reset run their real email flows: read the message in the web inbox and click the link. Nothing leaves the machine, and Mailpit keeps no volume, so `docker compose down` discards the captured mail. The resend-verification page does not work against `ds-spring-user-framework:5.3.0`: `UserAPI.resendRegistrationToken` binds the full registration `UserDto`, whose `firstName`, `lastName`, `password`, and `matchingPassword` are `@NotBlank`, so the email-only payload that `resend-verification.js` posts is rejected with HTTP 400 and no mail is sent. That is a framework-side bug, not a stack setting. - `user.mail.fromAddress` sets the `From` address on outbound mail; it is set per profile (e.g. `application-local.yml-example:144`), not in the base file. ## Security settings this demo sets diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 203bff6..29af78c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -70,12 +70,13 @@ that automate the tunnel + proxy pair for the first case. See: - [`compose.dev.yaml`](../compose.dev.yaml): database only. Started automatically by `bootRun`'s Docker Compose integration; not meant to be run directly, though `docker compose -f compose.dev.yaml up -d` works if you want the database without the app. -- [`compose.yaml`](../compose.yaml): the full demo stack: app + MariaDB + a relay-only mail - container. `docker compose up -d` builds the app image (multi-stage [`Dockerfile`](../Dockerfile): +- [`compose.yaml`](../compose.yaml): the full demo stack: app + MariaDB + a Mailpit mail catcher + whose web inbox is on . `docker compose up -d` builds the app image + (multi-stage [`Dockerfile`](../Dockerfile): a JDK-21 build stage runs `./gradlew --no-daemon bootJar -x test` (`Dockerfile:14`), a JRE-21 stage runs the resulting jar as a non-root user) and runs all three. The app container's healthcheck polls `GET /actuator/health` - (`compose.yaml:86-90`), the only actuator endpoint left unauthenticated. -- [`docker-compose-keycloak.yml`](../docker-compose-keycloak.yml): the same app + MariaDB + mail + (`compose.yaml:72-76`), the only actuator endpoint left unauthenticated. +- [`docker-compose-keycloak.yml`](../docker-compose-keycloak.yml): the same app + MariaDB + Mailpit setup plus a Keycloak container, for testing OIDC login. Start with `docker compose -f docker-compose-keycloak.yml up -d --build --wait`. See [`keycloak/README.md`](../keycloak/README.md) for ports, credentials, and the login walkthrough, and diff --git a/keycloak/README.md b/keycloak/README.md index 5e5c115..fb5e5f2 100644 --- a/keycloak/README.md +++ b/keycloak/README.md @@ -2,7 +2,7 @@ `docker-compose-keycloak.yml` runs the demo app against a Keycloak OIDC provider alongside the built-in form login. Four containers: the app, Keycloak 25.0.6, one MariaDB shared by both, and a -mail server. The image builds from source in Docker (no local Gradle build), but that first build +Mailpit mail catcher. The image builds from source in Docker (no local Gradle build), but that first build resolves dependencies and runs `bootJar` in the image: several minutes. Later starts are under a minute. `--wait` holds until every container is healthy; plain `up -d` returns mid-boot. @@ -16,6 +16,7 @@ docker compose -f docker-compose-keycloak.yml down -v # stop and delete the da | What | URL | Login | | --- | --- | --- | | Demo app | http://localhost:8080 | see below | +| Mailpit web inbox | http://localhost:8025 | none | | Keycloak | http://localhost:8180 | `admin` / `admin` (master realm) | | Keycloak HTTPS | https://localhost:8143 | self-signed, see `ssl/README.MD` | | Keycloak management | port 9001 (container 9000) | HTTPS, serves `/health/*` and `/metrics` | @@ -32,6 +33,18 @@ All of these are dev-only credentials committed to the repository. Do not reuse `admin` / `admin` is a master realm account for the admin console only, not a demo realm user. +## Register with the app's own form + +The Keycloak button is not the only way in: the app's built-in registration still works in this stack, +with verification email on. Register at http://localhost:8080/user/register.html, open the Mailpit inbox +at http://localhost:8025, click the verification link in the message, then log in at +http://localhost:8080/user/login.html. Password reset from +http://localhost:8080/user/forgot-password.html arrives in the same inbox. + +Mailpit captures everything the app sends and delivers nothing, so no address you type has to be real and +no mail leaves the machine. It stores messages in memory only: `down` discards them. Keycloak's own realm +has no SMTP server configured and sends nothing. + ## The realm `realm/realm-export.json` is imported by `--import-realm` on first start: realm `demo`, the client diff --git a/mailserver.env b/mailserver.env deleted file mode 100644 index e16c5ba..0000000 --- a/mailserver.env +++ /dev/null @@ -1,26 +0,0 @@ -# Basic Settings -DMS_DEBUG=1 -ONE_DIR=1 -SMTP_ONLY=1 -PERMIT_DOCKER=connected-networks -ENABLE_SPAMASSASSIN=0 -ENABLE_CLAMAV=0 -ENABLE_FAIL2BAN=0 -ENABLE_POSTGREY=0 -POSTFIX_INET_PROTOCOLS=ipv4 - -# Disable unnecessary services -ENABLE_POP3=0 -ENABLE_IMAP=0 -ENABLE_FETCHMAIL=0 -ENABLE_SASLAUTHD=0 -ENABLE_MANAGESIEVE=0 -ENABLE_SRS=0 - -# Allow all email sending/receiving for demo purposes -SPOOF_PROTECTION=0 -POSTFIX_MESSAGE_SIZE_LIMIT=0 -POSTSCREEN_ACTION=ignore - -# Logging for better demo visibility -SUPERVISOR_LOGLEVEL=debug diff --git a/src/main/resources/messages/messages.properties b/src/main/resources/messages/messages.properties index 201ccd1..34461c7 100644 --- a/src/main/resources/messages/messages.properties +++ b/src/main/resources/messages/messages.properties @@ -2,7 +2,7 @@ page.title.home=Home Page page.title.login=Log In page.title.registration=Register Your Account -page.title.registration-ending-verification=Registration Pending Verification +page.title.registration-pending-verification=Registration Pending Verification page.title.registration-resend-verification=Send New Verification Email page.title.registration-complete=Registration Complete! page.title.forgot-password=Forgot Your Password?