SharePointVault ~/gotchas/spfx ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ user-email-is-empty-for-mailbox-less-accounts.md
sharepoint-vault›gotchas›spfx›user-email-is-empty-for-mailbox-less-accounts.mdhistoryedit on GitHub
● GOTCHAspfx· 4 min read

Per-user data vanishes for admin accounts: pageContext.user.email is empty and eq '' matches nothing

applies to: SharePoint Online (SPFx web parts and extensions storing per-user rows in a list)reviewed: 2026-08-28#spfx#page-context#identity#rest-api#odata#filter#admin-accounts#lists

Accounts without a mailbox — typically the adm.* / service accounts you deploy and test with — get an empty string from context.pageContext.user.email. If that value is your identity key, every row you write lands with an empty field, SharePoint stores empty text as NULL, and your $filter=Field eq '' then matches nothing. The data is saved and unreadable at the same time. Key per-user data on email || UPN, never on the raw email.

Účty bez poštovní schránky – typicky adm.* a servisní účty, kterými nasazujete a testujete – dostanou z context.pageContext.user.email prázdný řetězec. Když je to váš klíč identity, uloží se každý řádek s prázdným polem, SharePoint prázdný text ukládá jako NULL a filtr $filter=Field eq '' pak nenajde nic. Data jsou uložená a zároveň nečitelná. Osobní data klíčujte na email || UPN, nikdy na holý e-mail.

Symptom

A tester reports it on one account and not another, which is what makes this expensive to diagnose:

  • On their regular account the feature works.
  • On their admin account, their own records "keep disappearing". They create something, it works for the rest of the session, and after a reload or after closing the panel it is gone.
  • Nothing is logged. No 4xx, no 5xx. The write returned 201 Created.

Looking in the list through the browser confirms the worst version of the confusion: the rows are there. So the app looks like it is deleting user data.

Cause

Two independent behaviours line up.

1. pageContext.user.email is empty for accounts with no mailbox. It is not a bug and not a permissions problem — the property is sourced from the user's mail attribute, and admin/service accounts routinely have none. loginName and displayName are still populated. A common shape of the defect is a "safe-looking" fallback:

this._userId = context.pageContext.user.email || '';   // <- empty string for adm.* accounts

2. In SharePoint, empty text is NULL, and eq '' does not match NULL. Writing { MyUserField: '' } to a single line of text column stores NULL. Reading it back with

/_api/web/lists/getbytitle('MyList')/items?$filter=MyUserField eq ''

returns 200 OK with an empty value array. Not an error — just nothing. Every "load my stuff" call quietly returns zero rows, and the UI has no way to tell that apart from "the user has nothing yet".

The combination means the account can write rows it can never read again.

Why the "healthy" account misleads you further

Because the admin account loads zero personal rows, it never restores whatever those rows would have triggered. Any expensive or buggy code path that only runs when personal data exists also never runs there. So the broken account can look healthier than the working one, and you will spend your time comparing permissions instead of comparing identity.

Fix

Key the identity on the UPN when the mailbox is missing. The UPN is present on every account and is stable and unique:

// SPFx gives a claim: i:0#.f|membership|user@domain -> the UPN is the last segment
const login = context.pageContext.user.loginName || '';
const upn = login.indexOf('|') !== -1 ? login.substring(login.lastIndexOf('|') + 1) : login;
const email = context.pageContext.user.email;
const mailbox = (typeof email === 'string' && email.indexOf('@') !== -1) ? email : '';

if (!mailbox) {
  // Not an error — a different kind of account. But it MUST be visible, because it
  // changes the key that all personal data is stored under.
  console.warn('[app] account without a mailbox — personal data identity falls back to UPN:', upn);
}
this._identity = mailbox || upn || '';

Notes that make this safe to retrofit:

  • It is backward compatible. For any account that has a mailbox, email || upn is still email, so existing stored rows keep matching. Only accounts for which nothing worked at all change behaviour.
  • Do not fall back to displayName. It is neither unique nor stable; it is fine as a label in a report, never as a key.
  • Fix every occurrence in one pass. This kind of identity is usually copied into several places (settings, history, preferences, per-user counters). Half a fix is worse than none, because the remaining half now looks deliberate.
  • If you genuinely want to query for blank values, ask for $filter=Field eq null. eq '' is not a query for "empty".
  • Make an empty identity loud. A read that returns zero rows because the key was empty should not look like "no data yet". Log it, or surface it in a diagnostic view.

Test for it

Add an account with no mailbox to your test matrix. It is not an exotic case: the person who installs the solution is usually an admin, so a mailbox-less account is frequently the first account that ever opens your app — and the first to hit this.

A one-line check from the browser console on a page where your solution runs:

// empty string here = your identity key is about to be empty too
_spPageContextInfo.userEmail

It is not only the OData filter — a client-side guard hides the row too (2026-08-28)

The eq '' mismatch above is the server-side half. The same empty key bites again entirely in the browser, and there the symptom is faster and more confusing: the record is written, shows up for the rest of the session, and is gone after a reload.

A saved-filter ("segments") feature stored an owner and rendered the list as:

// `me` is the identity key — empty string for a mailbox-less account
items.filter(s => s.shared || (!!me && s.owner === me));

The !!me guard exists so an unknown user does not match every unowned row — reasonable on its own. Combined with an empty key it means the author of a private item can never see it again: the row was written with owner: '' and the guard drops it before any comparison happens.

So when you fix an empty identity, grep for both shapes: the OData eq on the key and every client-side predicate that guards on the key being truthy. Fixing only the query leaves the second one silently filtering the user out of their own data.

Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub
last reviewed: 2026-08-28last updated: 2026-09-24historyedit on GitHubview source