User Email Validation & Normalization
This guide documents how Akinon Commerce validates and stores the user email field, and how email addresses are matched when a user registers, logs in, or places an order.
It exists to answer a specific, recurring question: why can two spellings of the same address (for example Ibrahim@example.com and ibrahim@example.com, or an address that starts with the Turkish letter İ) end up as two separate users, or fail to sync to an external system?
The short answer is that Commerce stores the email exactly as it was submitted (it does not lower‑case or otherwise normalize it on write), while matching an existing user is done case‑insensitively. Downstream systems (the channel_app integration library, an ERP, a marketplace) often compare email addresses case‑sensitively, so a difference in casing that Commerce ignores can still create a mismatch at the boundary.
1. Overview
Key characteristics:
The
emailfield is the standard DjangoAbstractUser.emailfield:EmailField,max_length=254. It is not database‑unique and has no database collation set.There is no normalization on save — no lower‑casing, no trimming, no Unicode normalization in the registration or order‑driven user‑creation path.
Validation is the default DRF
EmailField(RFC‑style syntax check). There is no configurable email regex or format setting — nothing analogous toUSER_PHONE_REGEX/USER_PHONE_FORMAT.Existing‑user lookups are almost always case‑insensitive (
email__iexact).The only paths that persist a lower‑cased email are the social / SSO login providers.
This combination — case‑preserving storage plus case‑insensitive matching — is the root of the behavior described in this guide.
2. Where the email is stored
The User model subclasses Django's AbstractUser and does not redefine email; the field is inherited as‑is:
# omnishop/users/migrations/0001_initial.py
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address'))Notes:
Not unique. There is no unique constraint on
emailat the database level, and no later migration adds one or sets a collation.No
save()override onUsertouches the email. The address is written exactly as received.Order records keep their own copy:
Order.user_emailis also a plainEmailField(not unique, no collation).The manager (
User.objects) is built from Django'sUserManager, socreate_user()/normalize_email()technically exist — but the registration and order flows do not call them. They go through django‑allauth adapters and DRF serializers instead (see §3).
3. Write behavior — casing is preserved
When a user registers, or when a user is created as a side effect of an order, the email is stored verbatim, with its original casing.
RegisterSerializer.validate_emailcalls the allauth adapter'sclean_email(). Commerce does not overrideclean_email, so it returns the address unchanged — no lower‑casing.The serializer then saves
validated_data['email']directly.The auto‑generated (“dummy”) username is a case‑sensitive SHA‑256 hash of the raw email. This means
S.malli@example.comands.malli@example.comproduce different usernames.
The one exception — social / SSO login. The social‑account providers (for example the akifast and turkcell providers) force the incoming email to lower‑case before it is persisted. So an account created through social login is stored lower‑cased, while the same person registering with the email/password form is stored with whatever casing they typed.
There is no
.capitalize(),.title(), or Turkish‑locale case change applied to the email or to the first/last name anywhere in the Commerce write path. If you see an email arrive at an external system with an unexpected capital first letter (for example a lower‑case address whose first letter has become the Turkishİ), that transformation happened outside Commerce — typically in the storefront input, thechannel_appintegration, or the receiving system. See §6.
4. Match / lookup behavior — case‑insensitive
While storage preserves case, looking up an existing user is case‑insensitive in almost all Commerce flows, using Django's email__iexact:
Registration existence check —
email_address_exists()filtersEmailAddressand thenUserwithemail__iexact.Resolving a user from an email —
get_user_from_email()usesemail__iexact; if more than one row matches it returnsNone(fail‑closed).Order → user matching — when an order is placed for a guest, the flow looks up
User.objects.guest().get(email__iexact=email). On a miss it creates the user with the raw, original‑case email.Checkout email guard and most service/adapter/form/filter lookups also use
email__iexact.
Exact‑match outliers. A few background paths compare with a plain email= (case‑sensitive) instead of iexact — notably the subscription task that resolves a user by exact email. In those paths a casing difference does cause a miss.
The asymmetry to remember:
Write (register, order‑driven create)
Preserved as submitted
Write (social / SSO login)
Lower‑cased
Existence / login / order matching
Case‑insensitive (email__iexact)
A few background tasks (e.g. subscribe)
Case‑sensitive (email=)
Username (dummy) generation
Case‑sensitive SHA‑256 of raw email
5. The Turkish İ (U+0130) case
The dotted capital İ (Unicode U+0130, LATIN CAPITAL LETTER I WITH DOT ABOVE) is the reason several email incidents are specifically about Turkish input.
Two facts make it special:
İis a distinct code point from ASCIII/i. An address that begins withİ(for exampleİrem@example.com) is a genuinely different byte sequence fromIrem@example.comorirem@example.com.Case‑folding
İdoes not produce a plain ASCIIi. In Python's locale‑independent default casing,"İ".lower()returns two characters —ifollowed by the combining dot aboveU+0307("i̇") — not the single character"i". Correspondingly"I".lower()is"i"and"ı".upper()is"I".
Consequences:
Because Commerce preserves case on write (§3), an address typed or auto‑capitalized as
İrem@example.comis stored with theİand passed on to integrations and the ERP unchanged.Case‑insensitive matching (
email__iexact) relies on the database's case‑folding for U+0130, which is collation‑dependent and will not reliably foldİto ASCIIi. Soİrem@…andirem@…may be treated as two different users even by the “case‑insensitive” path.A receiving system that validates addresses against an ASCII‑only pattern (for example an SAP/ERP endpoint) can reject
İ…@…as an invalid email even though Commerce accepted it.
This is why an order can be created in Commerce but then fail to sync, or why the same shopper appears twice: the divergence is in the casing of the address, and specifically in how İ is handled, not in the account logic itself.
6. Behavior across layers (Commerce, channel_app, Omnitron)
The email address crosses several systems, and they do not all treat casing the same way. This is the single most important thing to understand when diagnosing a mismatch.
Commerce (storefront APIs)
Preserves case (except social/SSO, which lower‑cases)
Case‑insensitive email__iexact (a few background tasks are case‑sensitive)
channel_app (integration library)
Passes the address through as received
Case‑sensitive comparison when matching the Omnitron customer to the marketplace order (see channel_app/omnitron/commands/orders/customers.py)
Omnitron / external ERP
Stores what it receives
Depends on the system; ERPs frequently apply their own ASCII/format validation and case‑sensitive matching
Because Commerce ignores casing on match but channel_app (and typical ERPs) do not, an address that Commerce happily treats as one user can be seen as a new or non‑matching customer at the integration boundary. That boundary comparison — not Commerce account creation — is where the duplicate or the failed order surfaces.
This layer inconsistency is documented current behavior, not a recommendation. Aligning normalization across the layers would be a product change and is out of scope for this document.
7. Validation & settings — what does not exist for email
For phone numbers there are dedicated dynamic settings — USER_PHONE_REGEX and USER_PHONE_FORMAT (see Dynamic Settings, entries 27 and 28). There is no email equivalent.
No
USER_EMAIL_REGEX/ no email format setting. The accepted character set is fixed to DRF's defaultEmailFieldsyntax validation; it cannot be tightened or loosened through a dynamic setting, and there is no configurable “expected format” string for email.USER_PROFILE_VALIDATIONSdoes not include an email validator. Its allowed validator classes are onlyomnishop.users.helpers.check_ordersandomnishop.users.helpers.check_unique_phone.Profile edits cannot change the email.
emailis a read‑only field on the profile serializer.
If a partner asks “what is the USER_PHONE_REGEX equivalent for email?”, the accurate answer is: there isn't one — email uses only standard syntax validation and is neither normalized nor format‑constrained by a setting.
8. Troubleshooting checklist
Use this when a shopper appears twice, an order does not attach to the expected account, or an order fails to sync to an ERP/marketplace with an email error.
Fetch the stored customer and read the email exactly:
GET /api/v1/channel/:channel_id/customers/?email=<address>
Compare the two spellings character by character, not by eye. Confirm whether they differ only in case (
S.malli@vss.malli@) or contain a Turkish letter — especiallyİ(U+0130) or dotlessı(U+0131) in the local part.Check which layer is reporting the mismatch.
Commerce alone (registration/login/order matching) is case‑insensitive — a pure casing difference should not create a second Commerce user here.
If the duplicate/rejection comes from
channel_appor the ERP, the boundary comparison is case‑sensitive — the casing difference Commerce ignored is what breaks the match. See §6.
Check for the
İtwo‑character fold. If matching “should” work but doesn't, remember thatİ.lower() isi+U+0307, and the database'siexactfolding forU+0130is collation‑dependent (§5).Remember the username hash. Two case‑different emails yield two different dummy usernames, so they are genuinely distinct user rows if both were written.
Fix at the data level when needed: update the affected customer's email in Omnitron to the casing the downstream system expects, then re‑trigger the sync. There is no Commerce setting that will normalize this automatically.
Related
Dynamic Settings —
USER_PHONE_REGEX(27),USER_PHONE_FORMAT(28),USER_PROFILE_VALIDATIONS(53.1)
Last updated
Was this helpful?

