Files
upstream-ory-kratos/x/email.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

70 lines
2.3 KiB
Go

// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package x
import (
"strings"
"unicode"
"golang.org/x/net/idna"
"github.com/ory/jsonschema/v3"
)
func init() {
// email_loose accepts non-RFC carrier addresses (e.g. "foo.@docomo.ne.jp")
// that the strict RFC 5322 "email" format rejects, while still rejecting
// values that are unsafe in an SMTP envelope or an email header.
jsonschema.Formats["email_loose"] = func(v interface{}) bool {
s, ok := v.(string)
if !ok {
// Non-string values are handled by the "type" keyword.
return true
}
return IsEmailAddress(s)
}
}
// emailLocalAtext is the RFC 5322 "atext" punctuation permitted in the local
// part. It deliberately excludes the RFC "specials" — "()<>[]:;@\,\"" — and all
// whitespace/control characters, which could break out of an SMTP envelope or
// inject an email header.
const emailLocalAtext = "!#$%&'*+/=?^_`{|}~.-"
// maxEmailAddressLength caps accepted addresses. 320 is the historical RFC 3696
// bound (64-octet local part + "@" + 255-octet domain).
const maxEmailAddressLength = 320
// IsEmailAddress reports whether s is shaped like an email address using a
// lenient, injection-safe check rather than a strict RFC 5322 parse: a local
// part of letters, digits, and atext punctuation (dot placement is not
// restricted, so carrier addresses like "foo.@docomo.ne.jp" pass), a single
// "@", and a domain that is a valid host name per idna.Lookup. It rejects
// control characters, whitespace, the RFC "specials" that enable SMTP-envelope
// or header injection, and addresses longer than 320 bytes. It is structural
// only and does not verify that the domain resolves or is deliverable.
func IsEmailAddress(s string) bool {
if len(s) == 0 || len(s) > maxEmailAddressLength {
return false
}
local, domain, ok := strings.Cut(s, "@")
if !ok || local == "" || domain == "" {
return false
}
for _, r := range local {
switch {
case unicode.IsLetter(r) || unicode.IsDigit(r):
case strings.ContainsRune(emailLocalAtext, r):
default:
return false
}
}
// idna.Lookup rejects any domain that is not a valid host name, so it also
// gates out embedded "@", control characters, whitespace, and specials.
if _, err := idna.Lookup.ToASCII(domain); err != nil {
return false
}
return true
}