# LUVEEDU Business Email Manager API — Documentation

**Version:** 1.0.0 · **Base URL:** `https://bemail.luveedu.cloud` · **Transport:** HTTPS only · **Body format:** `application/json`

A **branded mailbox hosting** API (think Google Workspace / Microsoft 365, but programmatic). Customers verify a domain, then create real `user@domain.com` mailboxes that are reachable via IMAP/POP3/SMTP from any standard client, plus a webmail at `https://webmail.luveedu.cloud`.

> **Compatible with `email.luveedu.cloud`:**
> - Same `email_manager` database.
> - Same **2-CNAME** onboarding (so a domain verified for sending bulk mail is automatically usable for business mailboxes).
> - Same Postfix/Dovecot stack on `parking.luveedu.cloud` (the MX target).
> - DKIM signing is shared (selector `luveedu`).
> - A PTR record already maps `188.245.148.184` → `parking.luveedu.cloud` (Hetzner rDNS).

```yaml
base_url: https://bemail.luveedu.cloud
api_prefix: /api
admin_key_header: X-API-Key
admin_api_key: bemail_admin_2d4c3a7f8b9e1d0c4a6b9c8e7d2f5a1b3c6e9d8f4a7b2c5e1d8f9a4b3c6e7d2f
admin_ip_allowlist: [171.50.171.29, 188.245.148.184, 157.90.244.133]
mx_host: parking.luveedu.cloud              # customers' MX record points here
imap_server: parking.luveedu.cloud          # IMAP/993, POP3/995 (cert matches hostname)
smtp_server: parking.luveedu.cloud          # Submission/587 (STARTTLS) + 465 (TLS)
webmail_url: https://webmail.luveedu.cloud
delivery_path: API -> Dovecot(MySQL) -> Postfix(LMTP) -> /var/mail/vhosts/<domain>/<user>
verification: 2 CNAMEs (same as email-manager) + MX (parking.luveedu.cloud)
public_endpoints: [GET /api/health, GET /api/locations]
```

---

## 1. Authentication

All `/api/*` endpoints **except** `/api/health` and `/api/locations` require **BOTH**:
1. Header `X-API-Key: <admin key>`.
2. Caller IP in `{171.50.171.29, 188.245.148.184, 157.90.244.133}`.

The admin key is the same contract used by `email.luveedu.cloud` (your panel calls both services with one key). Wrong key or wrong IP → `401`/`403`, and the IP is registered as a failure (≥10 fails/min → 1-hour ban, see §6).

---

## 2. DNS onboarding (customer-facing)

Customers add exactly **two CNAMEs and one MX** at their DNS provider — the values are **static and identical for every domain**:

| Type  | Host                              | Value                                              | Purpose                            |
|-------|-----------------------------------|----------------------------------------------------|------------------------------------|
| CNAME | `email.<domain>`                  | `smtp.email.luveedu.cloud`                         | ownership + Return-Path + SPF      |
| CNAME | `luveedu._domainkey.<domain>`     | `luveedu._domainkey.smtp.email.luveedu.cloud`      | DKIM public key (server-standard)  |
| MX    | `<domain>` (apex)                 | `parking.luveedu.cloud.` (priority **10**)          | mail delivery into our Postfix     |

Optional (recommended, any provider):
- Root TXT `v=spf1 include:_spf.luveedu.cloud ~all`
- `_dmarc.<domain>` TXT (suggested by `/api/domain-status`)

The 2 CNAMEs are **byte-identical** to those returned by `email.luveedu.cloud`'s `/api/add-domain`. So if a customer already verified their domain for sending, you can immediately start creating mailboxes against it.

---

## 3. Domain endpoints

### 3.1 `POST /api/create-domain`

```json
{ "domain": "your-domain.com", "user_id": "7", "username": "ariyan" }
```

**Response `200`:**

```json
{
  "id": 5,
  "domain": "your-domain.com",
  "status": "pending",
  "dns_records": {
    "cnames": [
      {"type":"CNAME","name":"email.your-domain.com",
       "value":"smtp.email.luveedu.cloud",
       "purpose":"ownership + Return-Path (bounces) + SPF"},
      {"type":"CNAME","name":"luveedu._domainkey.your-domain.com",
       "value":"luveedu._domainkey.smtp.email.luveedu.cloud",
       "purpose":"DKIM public key (server-standard shared key)"}
    ],
    "mx": {"type":"MX","name":"@ (your-domain.com)","priority":10,
           "value":"parking.luveedu.cloud.",
           "purpose":"Mail delivery — ALL inbound mail goes to parking.luveedu.cloud"},
    "optional": {
      "spf_root": {"type":"TXT","name":"@ (your-domain.com)",
                   "value":"v=spf1 include:_spf.luveedu.cloud ~all"},
      "dmarc_suggested": {"type":"TXT","name":"_dmarc.your-domain.com",
                          "value":"v=DMARC1; p=none; rua=mailto:dmarc@luveedu.cloud"}
    },
    "message": "Add the 2 CNAMEs AND the MX record at your DNS provider, then call /api/domain-status. These values are static."
  },
  "message": "Add BOTH CNAMEs and the MX record at your DNS provider, then call /api/domain-status to trigger verification."
}
```

Idempotent: re-creating the **same** `(domain, user_id, username)` returns the existing row.
Different owner → `409`.

### 3.2 `GET /api/domain-status?domain=...&user_id=&username=`

Re-runs the DNS check **live** against public resolvers (`1.1.1.1`, `8.8.8.8`) and updates the row's status.

**Response `200`:**

```json
{
  "domain": "your-domain.com",
  "status": "verified",                // "verified" | "failed"
  "verified": true,
  "checks": {
    "cname_ownership": true,           // email.<dom> -> smtp.email.luveedu.cloud
    "dkim_cname":      true,           // luveedu._domainkey.<dom> -> ...
    "mx_record":       true,           // MX apex -> parking.luveedu.cloud
    "mx_points_to":    "parking.luveedu.cloud"
  },
  "dns_records": { /* same shape as create-domain */ },
  "hint": null                         // present when any check fails
}
```

The row's `verified_at` is stamped on success. Until all three checks pass, the row stays at `status: "pending" | "failed"` and `/api/create-user` returns `409`.

### 3.3 `POST /api/remove-domain`

Same body as create. Removes the domain, **all** its mailboxes, and on-disk maildirs (`/var/mail/vhosts/<domain>/...`).

```json
{ "domain": "your-domain.com", "user_id": "7", "username": "ariyan" }
```

→ `{"deleted": "your-domain.com", "users_purged": ["info", "sales"]}`

### 3.4 `GET /api/list-domains[?user_id=&username=]`

Returns every domain row (or filtered). Each row includes `mailbox_count`:

```json
[
  {"id":5,"domain":"your-domain.com","user_id":"7","username":"ariyan",
   "status":"verified","created_at":"...","verified_at":"...",
   "mailbox_count":2}
]
```

---

## 4. Mailbox endpoints

> **Mailboxes can only be created on a `verified` domain.** `/api/create-user` returns `409` until the domain passes all three DNS checks.

### 4.1 `POST /api/create-user`

```json
{
  "domain":      "your-domain.com",
  "local_part":  "info",
  "password":    "<at least 8 chars>",
  "user_id":     "7",
  "username":    "ariyan",
  "quota_mb":    1024,                 // optional, default 1024, min 10, max 51200
  "display_name":"Info"
}
```

**Response `200`:**

```json
{
  "id": 11,
  "address": "info@your-domain.com",
  "display_name": "Info",
  "maildir": "/var/mail/vhosts/your-domain.com/info/Maildir",
  "quota_mb": 1024,
  "imap":  {"host":"parking.luveedu.cloud","port":993,"tls":true,
            "username":"info@your-domain.com"},
  "smtp":  {"host":"parking.luveedu.cloud","port":587,
            "starttls":true,"auth":true,
            "username":"info@your-domain.com"},
  "webmail": "https://webmail.luveedu.cloud/?user=info@your-domain.com",
  "note": "Password is set as provided. Use these credentials in any IMAP/SMTP client or the webmail at webmail.luveedu.cloud."
}
```

What happens server-side:
- Domain must exist for this owner AND be `verified`.
- Password is hashed with **bcrypt (`$2b$`)** — Dovecot is configured with `default_pass_scheme=BLF-CRYPT`, so the stored hash works directly with `dovecot-sql` without any extra translation.
- The maildir tree `/var/mail/vhosts/<domain>/<local>/Maildir` is created (with `cur/`, `new/`, `tmp/` and standard IMAP sub-folders `.Sent`, `.Drafts`, `.Trash`, `.Junk`) and chowned to `vmail:5000/5000`.

Duplicate `local_part@domain` → `409`.

### 4.2 `POST /api/reset-password`

```json
{
  "domain":     "your-domain.com",
  "local_part": "info",
  "password":   "<new password, ≥8 chars>",
  "user_id":    "7",
  "username":   "ariyan"
}
```

→ `{"reset": "info@your-domain.com"}`. Same bcrypt round.

### 4.3 `POST /api/delete-user`

```json
{
  "domain":     "your-domain.com",
  "local_part": "info",
  "user_id":    "7",
  "username":   "ariyan"
}
```

→ `{"deleted": "info@your-domain.com"}`. The on-disk maildir is purged.

### 4.4 `GET /api/list-users[?domain=&user_id=&username=]`

Returns mailbox rows. **Passwords are never returned.**

```json
[
  {"id":11,"domain":"your-domain.com","user_id":"7","username":"ariyan",
   "local_part":"info","full_address":"info@your-domain.com",
   "maildir_path":"/var/mail/vhosts/your-domain.com/info/Maildir",
   "quota_mb":1024,"is_active":true,
   "created_at":"...","updated_at":"..."}
]
```

---

## 5. Service endpoints

### 5.1 `GET /api/locations` (public)

```json
{
  "mx": "parking.luveedu.cloud",
  "smtp_server": "parking.luveedu.cloud",
  "imap_server": "parking.luveedu.cloud",
  "pop3_server": "parking.luveedu.cloud",
  "smtp_port": 587,
  "smtp_tls_port": 465,
  "imap_port": 993,
  "pop3_port": 995,
  "dkim_selector": "luveedu"
}
```

### 5.2 `GET /api/health` (public)

```json
{
  "status": "healthy",
  "service": "bemail-manager-api",
  "version": "1.0.0",
  "mx_host": "parking.luveedu.cloud",
  "imap_server": "parking.luveedu.cloud",
  "smtp_server": "parking.luveedu.cloud",
  "webmail_url": "https://webmail.luveedu.cloud",
  "smtp_local_ok": true,
  "imap_local_ok": true,
  "timestamp": "2026-08-25T10:34:00"
}
```

### 5.3 `GET /api/sync[?since=ISO]` (admin)

One-shot export of **all domains and mailboxes** for mirroring into a frontend panel database. Passwords are not exported. `since` filters to rows created after the given timestamp.

```json
{
  "service": "bemail-manager",
  "server_time": "2026-08-25T10:34:00",
  "counts": {"domains": 1, "mailboxes": 2},
  "domains":  [ /* full domain rows incl. mailboxes count */ ],
  "mailboxes":[ /* full mailbox rows, NO password_hash */ ],
  "mx_host": "parking.luveedu.cloud",
  "imap_server": "parking.luveedu.cloud",
  "smtp_server": "parking.luveedu.cloud",
  "webmail_url": "https://webmail.luveedu.cloud"
}
```

---

## 6. Mail flow

```
OUTBOUND (webmail/desktop sends a mail):
   Mail client (Roundcube / Outlook / Thunderbird)
     ─ STARTTLS+auth ─→  parking.luveedu.cloud:587
     ─ DKIM(rsa-sha256, d=your-domain.com) ─→ Postfix
     ─→  Internet MX

INBOUND (someone sends to info@your-domain.com):
   Internet
     ─ MX=parking.luveedu.cloud (priority 10) ─→  Postfix on 188.245.148.184
     ─ virtual_mailbox_maps (mysql:virtual-mailbox-maps.cf) ─→ info@your-domain.com
     ─ LMTP unix socket (private/dovecot-lmtp) ─→  Dovecot
     ─ auth-bemail-sql.conf.ext (bcrypt $2b$) ─→ mailbox lookup
     ─ maildir /var/mail/vhosts/your-domain.com/info/Maildir/new/
```

### Webmail

Roundcube at `https://webmail.luveedu.cloud` connects to `parking.luveedu.cloud` for IMAP/SMTP using the same mailbox credentials.

### Server operator TODOs (already done)

| Setting               | Value                                                  |
|-----------------------|--------------------------------------------------------|
| Postfix `myhostname`  | `parking.luveedu.cloud`                                |
| Postfix `myorigin`    | `parking.luveedu.cloud`                                |
| Postfix `mydomain`    | `luveedu.cloud`                                        |
| Postfix `mydestination` | `localhost` (only system mail; everything else virtual) |
| Postfix `inet_interfaces` | `all`                                              |
| Postfix `virtual_transport` | `lmtp:unix:private/dovecot-lmtp`                |
| Postfix `virtual_mailbox_domains` | `mysql:/etc/postfix/virtual-mailbox-domains.cf` |
| Postfix `virtual_mailbox_maps`   | `mysql:/etc/postfix/virtual-mailbox-maps.cf`   |
| Postfix SASL          | `dovecot` via `private/auth`                           |
| Dovecot mail_location | `maildir:/var/mail/vhosts/%d/%n/Maildir`               |
| Dovecot auth          | `auth-bemail-sql.conf.ext` (mysql + BLF-CRYPT)         |
| Dovecot SSL cert      | `/etc/letsencrypt/live/parking.luveedu.cloud/...`   |
| vmail user            | uid/gid `5000`, home `/var/mail/vhosts`, shell `/usr/sbin/nologin` |
| PTR (Hetzner rDNS)    | `188.245.148.184` → `parking.luveedu.cloud` ✓          |

---

## 7. Rate limits & abuse protection

All limits are enforced app-side and return **`429`** with `Retry-After` and body `{error, status_code, reason, retry_after}`.

| Tier         | Trigger                                                | Scope       | Block |
|--------------|--------------------------------------------------------|-------------|-------|
| DoS guard    | ≥100 req/s sustained for ≥10 s                         | IP          | 1 hour |
| Attacker guard | ≥10 failed requests/min (missing/wrong key, wrong IP, malformed body) | IP | 1 hour |

Blocks are in-memory: restarting `bemail-manager-api.service` clears them.

---

## 8. Standard integration algorithm

```
PANEL (admin key, allowlisted IP):
  1. POST /api/create-domain         → show customer the 2 CNAMEs + 1 MX
  2. Customer publishes DNS
  3. GET  /api/domain-status         → repeats; flips status to "verified"
  4. POST /api/create-user           → returns imap/smtp/webmail config
  5. POST /api/reset-password        → rotate password later
  6. POST /api/delete-user           → remove mailbox
  7. POST /api/remove-domain         → remove domain (cascades mailboxes)
  8. GET  /api/sync                  → mirror all rows into the panel DB

CUSTOMER (Roundcube / Outlook / Thunderbird):
  - IMAP: parking.luveedu.cloud:993 (SSL/TLS), username=full@domain
  - SMTP: parking.luveedu.cloud:587 (STARTTLS + auth), same credentials
  - Webmail: https://webmail.luveedu.cloud
```

### cURL — full happy path

```bash
AK="bemail_admin_2d4c3a7f8b9e1d0c4a6b9c8e7d2f5a1b3c6e9d8f4a7b2c5e1d8f9a4b3c6e7d2f"
B="https://bemail.luveedu.cloud/api"

# 1) register domain
curl -s -X POST $B/create-domain -H "X-API-Key: $AK" -H "Content-Type: application/json" \
  -d '{"domain":"your-domain.com","user_id":"7","username":"ariyan"}'

# 2) after DNS is live, verify (repeatable)
curl -s "$B/domain-status?domain=your-domain.com&user_id=7&username=ariyan" \
  -H "X-API-Key: $AK"

# 3) create a mailbox
curl -s -X POST $B/create-user -H "X-API-Key: $AK" -H "Content-Type: application/json" \
  -d '{"domain":"your-domain.com","local_part":"info",
       "password":"s3cure-Pa55!","user_id":"7","username":"ariyan",
       "display_name":"Info"}'

# 4) reset password later
curl -s -X POST $B/reset-password -H "X-API-Key: $AK" -H "Content-Type: application/json" \
  -d '{"domain":"your-domain.com","local_part":"info",
       "password":"new-Pa55!","user_id":"7","username":"ariyan"}'

# 5) delete a mailbox
curl -s -X POST $B/delete-user -H "X-API-Key: $AK" -H "Content-Type: application/json" \
  -d '{"domain":"your-domain.com","local_part":"info",
       "user_id":"7","username":"ariyan"}'

# 6) remove domain (cascades mailboxes)
curl -s -X POST $B/remove-domain -H "X-API-Key: $AK" -H "Content-Type: application/json" \
  -d '{"domain":"your-domain.com","user_id":"7","username":"ariyan"}'
```

---

## 9. Error responses

```json
{ "error": "<message>", "status_code": <code> }
```

| Code | Trigger |
|------|---------|
| `400` | Bad body / invalid domain or local-part / quota out of range / password too short |
| `401` | Missing or invalid admin key |
| `403` | Admin call from non-allowlisted IP |
| `404` | Unknown domain or mailbox for this owner |
| `409` | Domain belongs to a different owner / mailbox already exists / domain not yet verified |
| `429` | Rate-limit block (DoS or attacker guard) — see §7 |

CORS: all origins; headers `X-API-Key, Content-Type`; methods `GET, POST, OPTIONS`.

---

## 10. Compatibility with `email.luveedu.cloud`

Both services share the `email_manager` database, so a single domain record serves both roles:

```
email.luveedu.cloud (sending)         bemail.luveedu.cloud (receiving)
─────────────────────────────         ──────────────────────────────
/api/add-domain       ─┐
                       │               /api/create-domain
/api/verify-domain     ─┼─── same 2 CNAMEs ───┤
                       │               /api/domain-status   (adds MX check)
/api/send              │               /api/create-user
/api/status/{id}       │               /api/reset-password
                       │               /api/delete-user
/api/add-balance       │               /api/remove-domain
```

- One DKIM selector (`luveedu`) signed on outbound. Inbound uses LMTP — no DKIM re-check needed (Dovecot stores as-is).
- `parking.luveedu.cloud` is both the **MX** target and the **EHLO hostname** — every receiving server sees a hostname that matches the PTR.
- A customer's mailbox at `info@your-domain.com` can **receive** mail at the same domain they use for **sending** transactional/bulk mail through `email-manager`. No extra onboarding needed: verify once, send AND receive.
