# MTS INBOX — Tài liệu API (mail.metuansoc.com)

Hệ thống nhận thư cho các alias `@mail.metuansoc.com`, thay cho inbox.stravexunited.shop.
Nó tự trích mã xác minh (Facebook/Meta và mọi nơi khác) và link trong thư.

- **Giao diện web:** https://mail.metuansoc.com
- **API gốc:** `https://mail.metuansoc.com/api`
- **Tài liệu này:** https://mail.metuansoc.com/docs
- **Máy chủ:** VPS 157.10.201.102, thư mục `/opt/mailbox`, container `mailbox`.
  - SMTP nhận thư ở cổng 25.
  - Thư lưu 30 ngày.
  - Bản gốc `.eml` của từng thư nằm trong `/opt/mailbox/data/raw`.

---

## 1. Xác thực

Mọi đường dẫn `/api/*` (trừ `/api/health`) đều cần API key, gửi theo **một trong hai** cách sau:

```
Authorization: Bearer <MAILBOX_KEY>
X-Api-Key: <MAILBOX_KEY>
```

- Key nằm trong `/opt/mailbox/.env` trên VPS (biến `MAILBOX_KEY`).
- Xem key bằng lệnh: `ssh root@157.10.201.102 "grep MAILBOX_KEY /opt/mailbox/.env"`.
- Không đưa key vào URL.
- Sai key 20 lần trong 10 phút thì IP đó bị khoá 10 phút (lỗi 429).

**Đổi key:** sửa `/opt/mailbox/.env`, rồi chạy `cd /opt/mailbox && docker compose up -d`.

Định dạng dữ liệu:
- Request và response đều là JSON UTF-8.
- Thời gian là **epoch milliseconds** (số ms từ 1970, giờ UTC).
- Lỗi trả dạng `{"error": "..."}` kèm mã HTTP 400/401/404/409/429/500.

---

## 2. Cách dùng nhanh cho tool (lấy mã Facebook)

```
1. POST /api/aliases            -> tạo alias (hoặc random)  => lấy "address"
2. Dùng address đó trên Facebook để xin mã
3. GET  /api/codes/latest?alias=<address>&since=<thời điểm bước 2>&wait=90
   -> server giữ kết nối tối đa 90s, có mã là trả ngay
```

Ví dụ bằng curl:

```bash
KEY=xxxxxxxx
# 1) tạo alias ngẫu nhiên
curl -s -X POST https://mail.metuansoc.com/api/aliases \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"random":true,"label":"BM Lynn 01"}'
# => {"alias":{"address":"k3x9m2pq7z@mail.metuansoc.com", ...}}

# 2) ... dùng địa chỉ đó trên Facebook ...

# 3) chờ mã (tối đa 90 giây)
SINCE=$(($(date +%s)*1000 - 5000))
curl -s "https://mail.metuansoc.com/api/codes/latest?alias=k3x9m2pq7z@mail.metuansoc.com&since=$SINCE&wait=90&meta_only=1" \
  -H "Authorization: Bearer $KEY"
# => {"code":"482913","from":"security@facebookmail.com","verified_meta":true, ...}
```

Ví dụ Python:

```python
import time, requests
API, KEY = "https://mail.metuansoc.com/api", "xxxxxxxx"
H = {"Authorization": f"Bearer {KEY}"}
addr = requests.post(f"{API}/aliases", json={"random": True}, headers=H).json()["alias"]["address"]
since = int(time.time() * 1000)
# ... đưa addr cho Facebook ...
r = requests.get(f"{API}/codes/latest", params={"alias": addr, "since": since, "wait": 90, "meta_only": 1}, headers=H, timeout=100)
print(r.json().get("code"))   # None nếu hết 90s chưa có mã (HTTP 404)
```

Ví dụ JavaScript (Node 18+):

```js
const API = 'https://mail.metuansoc.com/api', H = { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' };
const { alias } = await (await fetch(`${API}/aliases`, { method: 'POST', headers: H, body: JSON.stringify({ local: 'meta-bm-01' }) })).json();
const since = Date.now();
const r = await fetch(`${API}/codes/latest?alias=${encodeURIComponent(alias.address)}&since=${since}&wait=90`, { headers: H });
const { code } = await r.json();
```

---

## 3. Danh sách endpoint

### 3.1. Trạng thái

| Method | Đường dẫn | Mô tả |
|---|---|---|
| GET | `/api/health` | Không cần key. `{"ok":true,"domains":[...],"smtp_tls":true,"time":...}` |
| GET | `/api/stats` | Tổng số thư, số chưa đọc, thư mới nhất, số alias, TLS, số ngày lưu. |
| GET | `/api/domains` | Domain nhận thư và trạng thái DNS. Thêm `?refresh=1` để kiểm tra lại ngay; bình thường hệ thống tự kiểm tra mỗi giờ. |

`/api/domains` trả về:

```json
{"domains":[{"domain":"mail.metuansoc.com","status":{
  "mx_ok":true,"mx_note":"MX trỏ về máy chủ này","mx":[{"exchange":"mail.metuansoc.com","priority":10,"ips":["157.10.201.102"]}],
  "host_a":["157.10.201.102"],"spf":null,"dmarc":null,"dmarc_org":null,"ptr":[...],"tls":true,"checked_at":1763100000000}}],
 "meta_sender_domains":["facebookmail.com","facebook.com","fb.com","meta.com","metamail.com","instagram.com"]}
```

`mx_ok=false` nghĩa là mail sẽ **không** tới được: kiểm tra bản ghi MX.

### 3.2. Alias

| Method | Đường dẫn | Body / query | Mô tả |
|---|---|---|---|
| GET | `/api/aliases` | | Trả `{"aliases":[...],"unregistered":[...]}`. Mỗi alias có `address, local, domain, label, created_at, messages, unread, last_message_at`. `unregistered` là địa chỉ đã nhận thư nhưng chưa tạo alias. |
| POST | `/api/aliases` | `{"local":"meta-bm","domain":"mail.metuansoc.com","label":"BM chính"}` hoặc `{"random":true}` | Tạo alias. `local` chỉ gồm a-z, 0-9, `.`, `_`, `-`, tối đa 64 ký tự. Bỏ trống `domain` thì dùng domain mặc định. Thành công trả **201** `{"alias":{...}}`; alias đã tồn tại trả **409** `{"error":..., "alias":{...}}`. |
| PATCH | `/api/aliases/{address}` | `{"label":"tên mới"}` | Đổi tên gợi nhớ. |
| DELETE | `/api/aliases/{address}` | | Xoá alias. Thư cũ vẫn giữ. |
| DELETE | `/api/aliases?confirm=yes` | | Xoá **tất cả** alias (thư vẫn giữ). |

`{address}` cần URL-encode, ví dụ `meta-bm%40mail.metuansoc.com`.

### 3.3. Thư

| Method | Đường dẫn | Query / body | Mô tả |
|---|---|---|---|
| GET | `/api/messages` | `alias`, `q`, `since`, `before`, `meta=1`, `unread=1`, `limit` (mặc định 50, tối đa 500) | Danh sách thư, mới nhất trước. `q` tìm trong tiêu đề, người gửi, người nhận, nội dung và mã. |
| GET | `/api/messages/{id}` | `mark_read=0` (không đánh dấu đã đọc) | Chi tiết một thư (xem 3.5). Mặc định đánh dấu là đã đọc. |
| GET | `/api/messages/{id}/raw` | | Tải bản gốc `.eml` (`message/rfc822`). |
| POST | `/api/messages/{id}/read` | `{"read":false}` để đánh dấu chưa đọc | Đánh dấu đã đọc / chưa đọc. |
| POST | `/api/messages/{id}/reextract` | | Đọc lại từ `.eml`, trích lại mã và link. |
| DELETE | `/api/messages/{id}` | | Xoá một thư (cả file `.eml`). |
| DELETE | `/api/messages?confirm=yes[&alias=...]` | | Xoá toàn bộ thư, hoặc chỉ thư của một alias nếu có `alias`. Trả `{"deleted":N,"scope":...}`. |

Mỗi thư trong danh sách:

```json
{"id":"20261114203512-a1b2c3d4e5","to":"meta-bm@mail.metuansoc.com","alias_known":true,
 "from":"security@facebookmail.com","from_name":"Facebook","subject":"482913 là mã xác nhận Facebook của bạn",
 "received_at":1763127312000,"sent_at":1763127310000,"code":"482913",
 "codes":[{"code":"482913","near":true}],"is_meta":true,"verified_meta":true,"dmarc":"pass",
 "is_read":false,"links_count":4}
```

### 3.4. Lấy mã mới nhất (dành cho tool)

`GET /api/codes/latest`

| Tham số | Ý nghĩa |
|---|---|
| `alias` | Chỉ lấy mã của địa chỉ này. Nên luôn truyền. |
| `since` | Chỉ lấy thư nhận **sau** thời điểm này (epoch ms). Truyền thời điểm bắt đầu xin mã để không lấy nhầm mã cũ. |
| `wait` | Số giây chờ, tối đa 120. Server kiểm tra mỗi 1 giây; có mã là trả ngay. |
| `meta_only=1` | Chỉ lấy thư từ domain Meta. |

Kết quả:
- **200:** `{"code":"482913","codes":[...],"message_id":"...","to":"...","from":"...","subject":"...","received_at":...,"is_meta":true,"verified_meta":true}`
- **404:** `{"code":null,"error":"Chưa có mã","waited_ms":90000}`.

Nếu có nhiều thư thì lấy thư **mới nhất**.

### 3.5. Chi tiết thư `GET /api/messages/{id}`

Ngoài các trường ở 3.3, chi tiết thư có thêm:

| Trường | Ý nghĩa |
|---|---|
| `text` | Nội dung dạng chữ. Nếu thư chỉ có phần HTML thì hệ thống tự chuyển HTML sang chữ. |
| `html_safe` | HTML đã lọc: bỏ script, sự kiện on*, `javascript:` và form; mọi link mở tab mới. |
| `links` | `[{"url","host","kind","meta"}]`. `kind` là `verify`, `help`, `unsubscribe`, `image` hoặc `other`. `meta=false` nghĩa là link không thuộc domain Meta. |
| `spf`, `dkim`, `dmarc` | Kết quả kiểm tra xác thực khi nhận thư. |
| `envelope_from`, `to_header`, `remote_ip`, `size`, `parse_error` | Thông tin kỹ thuật. |

---

## 4. Quy tắc trích mã và xác thực Meta

**Mã xác minh:**
- Là dãy 4–8 chữ số, hoặc dạng `FB-12345`.
- Chỉ tính khi đứng gần các từ như code, mã, xác minh, xác nhận, OTP, verification, security, login, đăng nhập, mật khẩu, password, reset, checkpoint, 2FA.
- Không lấy năm (19xx/20xx), giá tiền, mã bưu chính hay số điện thoại không đứng cạnh từ khoá.
- Hệ thống quét cả tiêu đề, phần `text/plain` và phần `text/html`.
- `code` là mã ưu tiên (gần từ khoá, ưu tiên 6 số). `codes` liệt kê tất cả mã tìm được.

**Mail Meta thật** khi đủ 2 điều:
1. Địa chỉ From thuộc `facebookmail.com`, `facebook.com`, `fb.com`, `meta.com`, `metamail.com` hoặc `instagram.com`, kể cả subdomain (ví dụ `business.facebook.com`).
2. **DMARC = pass**.

Khi đó thư có `verified_meta=true`. Thư có domain Meta mà DMARC không pass được gắn cảnh báo "có thể giả mạo". Facebook không bao giờ hỏi mật khẩu qua email.

**Nhận thư:**
- Hệ thống nhận mọi địa chỉ `@mail.metuansoc.com`, kể cả địa chỉ chưa tạo alias. Thư gửi tới địa chỉ chưa có alias được gắn cờ "chưa đăng ký" (`alias_known=false`) để không mất mã.
- Không chặn spam và không greylisting: thư được nhận ngay.
- Thư gửi tới domain khác bị từ chối (550), vì hệ thống không chuyển tiếp thư.

---

## 5. Đường thư vào và DNS (Cloudflare, metuansoc.com)

Bản ghi A cho giao diện web và API (luôn cần):

| Loại | Tên | Giá trị | Ghi chú |
|---|---|---|---|
| A | `mail` | `157.10.201.102` | để mây cam (Proxied) hay mây xám đều được |

Thư có thể vào hệ thống bằng một trong hai đường.

**Đường 1 — Cloudflare Email Routing (ĐANG CHẠY từ 14/11/2026; AZDIGI chặn cổng 25 chiều vào):**

Đã cài sẵn:
- Subdomain `mail.metuansoc.com` bật trong Email Routing; MX `route1-3.mx.cloudflare.net` do Cloudflare quản lý.
- Worker **mts-mailbox-email**, với Catch-all → Send to Worker đang Active.
- MX gốc của metuansoc.com (Namecheap) giữ nguyên.

Inbox nhận yêu cầu từ Worker khi thoả **một trong hai** điều kiện:
1. Header `CF-Worker: metuansoc.com` cộng IP thuộc dải của Cloudflare. Header này do Cloudflare tự gắn, code không giả được.
2. Đúng secret `INBOUND_KEY`.

Nếu inbox lỗi, Cloudflare trả **421 (tạm thời)** cho bên gửi và Facebook sẽ gửi lại sau.

Các bước cài lại từ đầu (chỉ khi cần):
1. Cloudflare → metuansoc.com → **Email** → **Email Routing**.
2. Thêm subdomain `mail.metuansoc.com`. Cloudflare tự tạo bản ghi MX và SPF cho subdomain này.
3. Workers & Pages → Create → Worker → dán file `cloudflare-email-worker.js` (có trong /opt/mailbox) → Deploy.
4. Trong Worker vừa tạo: Settings → Variables and Secrets → thêm **Secret** `INBOUND_KEY`. Giá trị lấy bằng: `ssh root@157.10.201.102 "grep INBOUND_KEY /opt/mailbox/.env"`.
5. Email Routing → **Routing rules** → **Catch-all** cho `mail.metuansoc.com` → Action: **Send to a Worker** → chọn worker đó → bật.

Worker nhận thư rồi POST bản gốc tới `https://mail.metuansoc.com/api/inbound`. Đường này chỉ dành cho Worker; tool không cần gọi.

**Đường 2 — SMTP trực tiếp cổng 25 (khi AZDIGI mở cổng 25 chiều vào):**

| Loại | Tên | Giá trị |
|---|---|---|
| A | `mail` | `157.10.201.102` (**DNS only**, mây xám) |
| MX | `mail` | `mail.metuansoc.com`, priority `10` |
| TXT | `mail` | `v=spf1 -all` (tuỳ chọn) |

Chỉ dùng **một** đường tại một thời điểm, vì bản ghi MX quyết định thư đi đường nào.

Kiểm tra lại DNS bằng các lệnh sau:

```bash
dig MX mail.metuansoc.com +short
dig A mail.metuansoc.com +short
```

Hoặc xem trên giao diện: bấm vào chấm "MX OK / MX lỗi" trên thanh trên cùng.

---

## 6. Vận hành

```bash
cd /opt/mailbox
docker compose logs -f --tail 50             # log: mỗi thư nhận được một dòng "IN ..."
docker compose up -d --build                 # build lại / khởi động lại
docker exec mailbox node selftest.mjs        # tự kiểm tra: gửi 1 thư mẫu kiểu Meta qua SMTP rồi đọc lại qua API
```

- **Dữ liệu:** `/opt/mailbox/data/mailbox.sqlite` cùng thư mục `raw/*.eml`.
- **Mã nguồn:** sao lưu trên GitHub repo `vps-ops`, thư mục `mailbox/`.
- **STARTTLS:** dùng chứng chỉ Caddy cấp cho mail.metuansoc.com. Container tự khởi động lại khi chứng chỉ được cấp hoặc gia hạn.
- **Nút trên giao diện:**
  - **Lock:** xoá API key khỏi trình duyệt và quay về màn hình nhập key.
  - **API:** mở tài liệu này.
  - **🔔:** bật/tắt tiếng báo khi có thư mới.
