# cPanel UAPI Contract (as implemented)

Everything here is implemented in `services/cpanelRealService.js`. This documents the request
shape sent and the response shape actually returned, confirmed against a real live cPanel account
(web10-cpn.neohosting.id, cPanel 11.x era / EA4, tested 2026-07-23) — not just documented
behavior.

All requests: `GET {CPANEL_URL}/execute/Email/{function}` with header
`Authorization: cpanel {username}:{api_token}`.

## `Email::list_pops`

Used for the list page, dashboard stats aggregation, and export.

Request params sent:
- `domain` — required.
- `api.paginate=1`, `api.paginate_start`, `api.paginate_size` — offset/limit pagination.
- `api.sort=1`, `api.sort_column_1`, `api.sort_method_1` (`lexicographic` /
  `lexicographic-reverse`) — server-side sort.
- `api.filter=1`, `api.filter_column_0=email`, `api.filter_type_0=contains`,
  `api.filter_term_0={search}` — only sent when a search term is present. Confirmed working: a
  non-matching term correctly returns `total_results: 0`.

**Actual confirmed response shape** — notably different from what's commonly documented:

```json
// A genuine mailbox (created via add_pop):
{"email": "user@domain.tld", "login": "user@domain.tld", "suspended_login": 0, "suspended_incoming": 0}

// The synthetic "Main Account" row cPanel always includes (not a real mailbox — the cPanel
// account's own default address):
{"login": "Main Account", "email": "cpanelusername", "suspended_login": 0, "suspended_incoming": 0}
```

Key findings, all confirmed live (not assumed):
- **No `domain` field at all**, on either row type — `mapPop()` derives it by splitting `email`
  on `@` instead of reading `raw.domain`.
- **No `diskquota`/`_diskquota` or `diskused`/`_diskused` fields** in a plain `list_pops` call on
  this server. Quota/usage render as "Unlimited" / 0 MB — this is missing upstream data, not a
  bug in `mapPop()`. If your server returns these fields, `mapPop()` already reads them
  opportunistically (`raw.diskquota ?? raw._diskquota`, etc.) and will pick them up automatically;
  it just has nothing to read here. Getting real usage numbers would need a separate per-mailbox
  API call (not yet implemented — would add an N+1 API call per row, which conflicts with the
  50k-account performance goal, so this needs a deliberate design decision before adding it, e.g.
  a batched/cached follow-up call rather than one per row).
- **The "Main Account" row cannot be filtered server-side.** Tried `api.filter_column_X=domain,
  type=eq` — cPanel rejected it outright: `"The following filter did not pertain to this dataset:
  domain eq \"...\""` (because that row has no `domain` key for the filter engine to check against).
  Filtering happens client-side instead, matching on `login === 'Main Account'` (a literal,
  reliable sentinel value, confirmed stable across both an empty-mailbox domain and a domain with
  real mailboxes).
- Because of the above, `total` from `api.paginate.total_results` is 1 higher than the real
  mailbox count on whichever page contains the Main Account row. Cosmetic off-by-one, accepted
  rather than engineered around (see `services/cpanelRealService.js` comments).
- Pagination metadata key is **`total_results`**, not `total_records` as originally assumed —
  this was a real bug (pagination silently fell back to `data.length` on every real call) caught
  and fixed only once tested against a live account.

## `Email::add_pop`

Params: `domain`, `email`, `password`, `quota` (MB, `0` = unlimited). **Confirmed live**: cPanel
enforces its own password strength meter server-side (independent of this app's "min 8 chars"
validation) — a technically-8+-character password like `Password123` was rejected with
`"...strength rating of "1"... enter a password with a strength rrating of "65" or higher"`. This
surfaces correctly as a 422 with cPanel's own message; no app-side change needed, but worth
knowing so a rejected weak password isn't mistaken for a bug.

## `Email::delete_pop`

Params: `domain`, `email`. Confirmed live — works as documented.

## `Email::passwd_pop`

Params: `domain`, `email`, `password`. Not yet exercised against a live mailbox (only add_pop/
delete_pop/list_pops have been); same password-strength caveat as add_pop likely applies.

## `Email::edit_pop_quota`

Params: `domain`, `email`, `quota` (MB, `0` = unlimited). Not yet exercised against a live mailbox.

## Error shape

All of the above are expected to return `{ status: 1, data: ... }` on success or
`{ status: 0, errors: [...] }` (or `{ error: '...' }`) on failure. `callUapi()` throws
`CpanelApiError` with the joined error messages for any non-1 status or network failure. Confirmed
live for both a validation-style rejection (weak password) and a full success.

## Still unverified

- `passwd_pop` and `edit_pop_quota` haven't been exercised against a real mailbox yet (only
  add_pop/delete_pop/list_pops have). Should behave the same way given the shared `callUapi()`
  path, but worth a real test before fully trusting them.
- Whether disk quota/usage data is obtainable at all from this cPanel install, and if so via what
  call — not resolved, see the list_pops note above.
