Spaces:
Paused
Paused
feat: admin panel overhaul — self-contained CSS, SMTP send, redesigned homepage
Browse files- Rewrite admin.html: remove Tailwind CDN dependency, all show/hide via
inline style.display (fixes broken modals, tabs, mailbox in China)
- Add outlook_smtp.py: send email via Outlook SMTP with STARTTLS
- Add POST /admin/api/accounts/{id}/send endpoint
- Add GET /admin/api/public-stats (no auth) for homepage stats
- Redesign index.html: gradient bg, colored feature cards, remove
redundant ReDoc link
- Add SMTP config (smtp_host/smtp_port) to config.py and .env.example
- Fix README: correct default password, add send endpoint docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- .env.example +5 -1
- README.md +8 -3
- outlook2api/admin_routes.py +48 -0
- outlook2api/config.py +2 -0
- outlook2api/outlook_smtp.py +71 -0
- outlook2api/static/admin.html +713 -636
- outlook2api/static/index.html +148 -40
.env.example
CHANGED
|
@@ -2,7 +2,11 @@
|
|
| 2 |
OUTLOOK2API_JWT_SECRET=change-me-in-production
|
| 3 |
OUTLOOK2API_HOST=0.0.0.0
|
| 4 |
OUTLOOK2API_PORT=8001
|
| 5 |
-
ADMIN_PASSWORD=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
# === Database ===
|
| 8 |
# SQLite (default, no setup needed):
|
|
|
|
| 2 |
OUTLOOK2API_JWT_SECRET=change-me-in-production
|
| 3 |
OUTLOOK2API_HOST=0.0.0.0
|
| 4 |
OUTLOOK2API_PORT=8001
|
| 5 |
+
ADMIN_PASSWORD=bk@3fd3E
|
| 6 |
+
|
| 7 |
+
# === SMTP (for sending email from admin panel) ===
|
| 8 |
+
OUTLOOK_SMTP_HOST=smtp-mail.outlook.com
|
| 9 |
+
OUTLOOK_SMTP_PORT=587
|
| 10 |
|
| 11 |
# === Database ===
|
| 12 |
# SQLite (default, no setup needed):
|
README.md
CHANGED
|
@@ -5,7 +5,7 @@ Mail.tm-compatible REST API for Outlook/Hotmail/Live accounts with admin panel a
|
|
| 5 |
## Features
|
| 6 |
|
| 7 |
- **Mail API** — Mail.tm-compatible Hydra API endpoints (domains, accounts, token, messages)
|
| 8 |
-
- **Admin Panel** — Web UI for account management, bulk import/export, stats dashboard
|
| 9 |
- **Batch Registration** — Automated Outlook account creation via GitHub Actions
|
| 10 |
- **CI Auto-Import** — Registered accounts automatically imported to admin panel
|
| 11 |
- **Verification Code Extraction** — `GET /messages/{id}/code` extracts OTP from emails
|
|
@@ -20,7 +20,7 @@ pip install -r requirements-api.txt
|
|
| 20 |
python -m outlook2api.app
|
| 21 |
|
| 22 |
# Open http://localhost:8001 for homepage
|
| 23 |
-
# Open http://localhost:8001/admin for admin panel (default password:
|
| 24 |
```
|
| 25 |
|
| 26 |
### Docker
|
|
@@ -58,6 +58,8 @@ docker compose up -d outlook2api
|
|
| 58 |
| PATCH | `/admin/api/accounts/{id}` | Toggle active / update notes |
|
| 59 |
| DELETE | `/admin/api/accounts/{id}` | Delete account |
|
| 60 |
| GET | `/admin/api/accounts/{id}/password` | Reveal password |
|
|
|
|
|
|
|
| 61 |
| GET | `/admin/api/export` | Export all active accounts |
|
| 62 |
|
| 63 |
## CI Auto-Import
|
|
@@ -80,10 +82,12 @@ gh workflow run register-outlook.yml -f count=5 -f threads=1
|
|
| 80 |
| Name | Default | Description |
|
| 81 |
|------|---------|-------------|
|
| 82 |
| `OUTLOOK2API_JWT_SECRET` | `change-me-in-production` | JWT signing secret |
|
| 83 |
-
| `ADMIN_PASSWORD` | `
|
| 84 |
| `DATABASE_URL` | `sqlite+aiosqlite:///./data/outlook2api.db` | Database URL |
|
| 85 |
| `OUTLOOK2API_HOST` | `0.0.0.0` | API bind host |
|
| 86 |
| `OUTLOOK2API_PORT` | `8001` | API bind port |
|
|
|
|
|
|
|
| 87 |
|
| 88 |
## HuggingFace Deployment
|
| 89 |
|
|
@@ -101,6 +105,7 @@ outlook2api/
|
|
| 101 |
│ ├── auth.py # JWT auth helpers
|
| 102 |
│ ├── config.py # Environment config
|
| 103 |
│ ├── outlook_imap.py # IMAP client
|
|
|
|
| 104 |
│ ├── store.py # Legacy JSON file store
|
| 105 |
│ └── static/ # Frontend
|
| 106 |
│ ├── index.html # Homepage
|
|
|
|
| 5 |
## Features
|
| 6 |
|
| 7 |
- **Mail API** — Mail.tm-compatible Hydra API endpoints (domains, accounts, token, messages)
|
| 8 |
+
- **Admin Panel** — Web UI for account management, bulk import/export, webmail with compose/reply, stats dashboard
|
| 9 |
- **Batch Registration** — Automated Outlook account creation via GitHub Actions
|
| 10 |
- **CI Auto-Import** — Registered accounts automatically imported to admin panel
|
| 11 |
- **Verification Code Extraction** — `GET /messages/{id}/code` extracts OTP from emails
|
|
|
|
| 20 |
python -m outlook2api.app
|
| 21 |
|
| 22 |
# Open http://localhost:8001 for homepage
|
| 23 |
+
# Open http://localhost:8001/admin for admin panel (default password: bk@3fd3E)
|
| 24 |
```
|
| 25 |
|
| 26 |
### Docker
|
|
|
|
| 58 |
| PATCH | `/admin/api/accounts/{id}` | Toggle active / update notes |
|
| 59 |
| DELETE | `/admin/api/accounts/{id}` | Delete account |
|
| 60 |
| GET | `/admin/api/accounts/{id}/password` | Reveal password |
|
| 61 |
+
| GET | `/admin/api/accounts/{id}/messages` | Fetch messages via IMAP |
|
| 62 |
+
| POST | `/admin/api/accounts/{id}/send` | Send email via SMTP |
|
| 63 |
| GET | `/admin/api/export` | Export all active accounts |
|
| 64 |
|
| 65 |
## CI Auto-Import
|
|
|
|
| 82 |
| Name | Default | Description |
|
| 83 |
|------|---------|-------------|
|
| 84 |
| `OUTLOOK2API_JWT_SECRET` | `change-me-in-production` | JWT signing secret |
|
| 85 |
+
| `ADMIN_PASSWORD` | `bk@3fd3E` | Admin panel password |
|
| 86 |
| `DATABASE_URL` | `sqlite+aiosqlite:///./data/outlook2api.db` | Database URL |
|
| 87 |
| `OUTLOOK2API_HOST` | `0.0.0.0` | API bind host |
|
| 88 |
| `OUTLOOK2API_PORT` | `8001` | API bind port |
|
| 89 |
+
| `OUTLOOK_SMTP_HOST` | `smtp-mail.outlook.com` | SMTP server host |
|
| 90 |
+
| `OUTLOOK_SMTP_PORT` | `587` | SMTP server port |
|
| 91 |
|
| 92 |
## HuggingFace Deployment
|
| 93 |
|
|
|
|
| 105 |
│ ├── auth.py # JWT auth helpers
|
| 106 |
│ ├── config.py # Environment config
|
| 107 |
│ ├── outlook_imap.py # IMAP client
|
| 108 |
+
│ ├── outlook_smtp.py # SMTP client (send email)
|
| 109 |
│ ├── store.py # Legacy JSON file store
|
| 110 |
│ └── static/ # Frontend
|
| 111 |
│ ├── index.html # Homepage
|
outlook2api/admin_routes.py
CHANGED
|
@@ -16,6 +16,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
| 16 |
from outlook2api.config import get_config
|
| 17 |
from outlook2api.database import Account, get_db, get_stats
|
| 18 |
from outlook2api.outlook_imap import fetch_messages_imap
|
|
|
|
| 19 |
|
| 20 |
admin_router = APIRouter(prefix="/admin/api", tags=["admin"])
|
| 21 |
|
|
@@ -52,6 +53,16 @@ class BulkImportRequest(BaseModel):
|
|
| 52 |
source: str = "import"
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
@admin_router.post("/login")
|
| 56 |
async def admin_login(body: LoginRequest):
|
| 57 |
cfg = get_config()
|
|
@@ -61,6 +72,13 @@ async def admin_login(body: LoginRequest):
|
|
| 61 |
return {"token": token}
|
| 62 |
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
@admin_router.get("/stats")
|
| 65 |
async def admin_stats(
|
| 66 |
request: Request,
|
|
@@ -297,3 +315,33 @@ async def get_account_messages(
|
|
| 297 |
except Exception as e:
|
| 298 |
raise HTTPException(status_code=502, detail=f"IMAP error: {e}")
|
| 299 |
return {"email": account.email, "messages": messages}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
from outlook2api.config import get_config
|
| 17 |
from outlook2api.database import Account, get_db, get_stats
|
| 18 |
from outlook2api.outlook_imap import fetch_messages_imap
|
| 19 |
+
from outlook2api.outlook_smtp import send_email
|
| 20 |
|
| 21 |
admin_router = APIRouter(prefix="/admin/api", tags=["admin"])
|
| 22 |
|
|
|
|
| 53 |
source: str = "import"
|
| 54 |
|
| 55 |
|
| 56 |
+
class SendEmailRequest(BaseModel):
|
| 57 |
+
to: str
|
| 58 |
+
subject: str
|
| 59 |
+
body_text: str = ""
|
| 60 |
+
body_html: str = ""
|
| 61 |
+
cc: str = ""
|
| 62 |
+
in_reply_to: str = ""
|
| 63 |
+
references: str = ""
|
| 64 |
+
|
| 65 |
+
|
| 66 |
@admin_router.post("/login")
|
| 67 |
async def admin_login(body: LoginRequest):
|
| 68 |
cfg = get_config()
|
|
|
|
| 72 |
return {"token": token}
|
| 73 |
|
| 74 |
|
| 75 |
+
@admin_router.get("/public-stats")
|
| 76 |
+
async def public_stats(db: AsyncSession = Depends(get_db)):
|
| 77 |
+
"""Public stats (no auth) — total and active account counts."""
|
| 78 |
+
stats = await get_stats(db)
|
| 79 |
+
return {"total": stats.get("total", 0), "active": stats.get("active", 0)}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
@admin_router.get("/stats")
|
| 83 |
async def admin_stats(
|
| 84 |
request: Request,
|
|
|
|
| 315 |
except Exception as e:
|
| 316 |
raise HTTPException(status_code=502, detail=f"IMAP error: {e}")
|
| 317 |
return {"email": account.email, "messages": messages}
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
@admin_router.post("/accounts/{account_id}/send")
|
| 321 |
+
async def send_account_email(
|
| 322 |
+
account_id: str,
|
| 323 |
+
body: SendEmailRequest,
|
| 324 |
+
request: Request,
|
| 325 |
+
db: AsyncSession = Depends(get_db),
|
| 326 |
+
):
|
| 327 |
+
"""Send an email from an account via SMTP."""
|
| 328 |
+
_verify_admin(request)
|
| 329 |
+
account = (await db.execute(select(Account).where(Account.id == account_id))).scalar_one_or_none()
|
| 330 |
+
if not account:
|
| 331 |
+
raise HTTPException(status_code=404, detail="Account not found")
|
| 332 |
+
try:
|
| 333 |
+
result = await asyncio.to_thread(
|
| 334 |
+
send_email,
|
| 335 |
+
from_addr=account.email,
|
| 336 |
+
password=account.password,
|
| 337 |
+
to_addr=body.to,
|
| 338 |
+
subject=body.subject,
|
| 339 |
+
body_text=body.body_text,
|
| 340 |
+
body_html=body.body_html,
|
| 341 |
+
cc=body.cc,
|
| 342 |
+
in_reply_to=body.in_reply_to,
|
| 343 |
+
references=body.references,
|
| 344 |
+
)
|
| 345 |
+
except RuntimeError as e:
|
| 346 |
+
raise HTTPException(status_code=502, detail=str(e))
|
| 347 |
+
return result
|
outlook2api/config.py
CHANGED
|
@@ -18,4 +18,6 @@ def get_config() -> dict:
|
|
| 18 |
"DATABASE_URL",
|
| 19 |
"sqlite+aiosqlite:///./data/outlook2api.db",
|
| 20 |
),
|
|
|
|
|
|
|
| 21 |
}
|
|
|
|
| 18 |
"DATABASE_URL",
|
| 19 |
"sqlite+aiosqlite:///./data/outlook2api.db",
|
| 20 |
),
|
| 21 |
+
"smtp_host": os.environ.get("OUTLOOK_SMTP_HOST", "smtp-mail.outlook.com"),
|
| 22 |
+
"smtp_port": int(os.environ.get("OUTLOOK_SMTP_PORT", "587")),
|
| 23 |
}
|
outlook2api/outlook_smtp.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Outlook SMTP client for sending emails."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import smtplib
|
| 6 |
+
from email.mime.multipart import MIMEMultipart
|
| 7 |
+
from email.mime.text import MIMEText
|
| 8 |
+
|
| 9 |
+
from outlook2api.config import get_config
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def send_email(
|
| 13 |
+
from_addr: str,
|
| 14 |
+
password: str,
|
| 15 |
+
to_addr: str,
|
| 16 |
+
subject: str,
|
| 17 |
+
body_text: str = "",
|
| 18 |
+
body_html: str = "",
|
| 19 |
+
cc: str = "",
|
| 20 |
+
in_reply_to: str = "",
|
| 21 |
+
references: str = "",
|
| 22 |
+
) -> dict:
|
| 23 |
+
"""Send an email via Outlook SMTP with STARTTLS.
|
| 24 |
+
|
| 25 |
+
Returns dict with status, from, to, subject on success.
|
| 26 |
+
Raises RuntimeError on failure.
|
| 27 |
+
"""
|
| 28 |
+
cfg = get_config()
|
| 29 |
+
smtp_host = cfg.get("smtp_host", "smtp-mail.outlook.com")
|
| 30 |
+
smtp_port = cfg.get("smtp_port", 587)
|
| 31 |
+
|
| 32 |
+
msg = MIMEMultipart("alternative")
|
| 33 |
+
msg["From"] = from_addr
|
| 34 |
+
msg["To"] = to_addr
|
| 35 |
+
msg["Subject"] = subject
|
| 36 |
+
|
| 37 |
+
if cc:
|
| 38 |
+
msg["Cc"] = cc
|
| 39 |
+
if in_reply_to:
|
| 40 |
+
msg["In-Reply-To"] = in_reply_to
|
| 41 |
+
if references:
|
| 42 |
+
msg["References"] = references
|
| 43 |
+
|
| 44 |
+
if body_text:
|
| 45 |
+
msg.attach(MIMEText(body_text, "plain", "utf-8"))
|
| 46 |
+
if body_html:
|
| 47 |
+
msg.attach(MIMEText(body_html, "html", "utf-8"))
|
| 48 |
+
elif body_text:
|
| 49 |
+
# If no HTML provided, send text as the only part
|
| 50 |
+
pass
|
| 51 |
+
else:
|
| 52 |
+
msg.attach(MIMEText("", "plain", "utf-8"))
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
|
| 56 |
+
server.ehlo()
|
| 57 |
+
server.starttls()
|
| 58 |
+
server.ehlo()
|
| 59 |
+
server.login(from_addr, password)
|
| 60 |
+
recipients = [to_addr]
|
| 61 |
+
if cc:
|
| 62 |
+
recipients.extend(a.strip() for a in cc.split(",") if a.strip())
|
| 63 |
+
server.sendmail(from_addr, recipients, msg.as_string())
|
| 64 |
+
except smtplib.SMTPAuthenticationError as e:
|
| 65 |
+
raise RuntimeError(f"SMTP authentication failed: {e}") from e
|
| 66 |
+
except smtplib.SMTPException as e:
|
| 67 |
+
raise RuntimeError(f"SMTP error: {e}") from e
|
| 68 |
+
except Exception as e:
|
| 69 |
+
raise RuntimeError(f"Failed to send email: {e}") from e
|
| 70 |
+
|
| 71 |
+
return {"status": "sent", "from": from_addr, "to": to_addr, "subject": subject}
|
outlook2api/static/admin.html
CHANGED
|
@@ -9,788 +9,865 @@
|
|
| 9 |
<style>
|
| 10 |
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 11 |
:root{
|
| 12 |
-
--bg:#
|
| 13 |
-
--text:#1a1a1a;--
|
| 14 |
-
--
|
| 15 |
-
--
|
| 16 |
-
--
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
html{font-family:'DM Sans',system-ui,-apple-system,sans-serif;font-size:15px;color:var(--text);background:var(--bg)}
|
| 19 |
body{min-height:100vh}
|
| 20 |
-
a{color:var(--
|
| 21 |
-
|
| 22 |
-
input,textarea,select
|
| 23 |
-
input:focus,textarea:focus,select:focus{border-color:var(--accent)}
|
| 24 |
textarea{resize:vertical}
|
|
|
|
| 25 |
|
| 26 |
/* ---- Buttons ---- */
|
| 27 |
-
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.
|
| 28 |
-
.btn-
|
| 29 |
-
.btn-
|
| 30 |
-
.btn-
|
| 31 |
-
.btn-
|
| 32 |
-
.btn:disabled{opacity:.
|
| 33 |
-
|
| 34 |
-
/* ---- Badge ---- */
|
| 35 |
-
.badge{display:inline-block;padding:.15rem .55rem;border-radius:99px;font-size:.75rem;font-weight:600}
|
| 36 |
-
.badge-active{background:rgba(58,138,92,.1);color:var(--success)}
|
| 37 |
-
.badge-inactive{background:rgba(196,64,64,.08);color:var(--danger)}
|
| 38 |
-
|
| 39 |
-
/* ---- Method badge ---- */
|
| 40 |
-
.method{display:inline-block;padding:.1rem .45rem;border-radius:4px;font-size:.7rem;font-weight:700;font-family:monospace;min-width:52px;text-align:center}
|
| 41 |
-
.method-get{background:#e8f5e9;color:#2e7d32}
|
| 42 |
-
.method-post{background:#e3f2fd;color:#1565c0}
|
| 43 |
-
.method-patch{background:#fff3e0;color:#e65100}
|
| 44 |
-
.method-delete{background:#fce4ec;color:#c62828}
|
| 45 |
|
| 46 |
/* ---- Toast ---- */
|
| 47 |
-
#toast-
|
| 48 |
-
.toast{padding:.
|
| 49 |
-
.toast-
|
| 50 |
-
@keyframes
|
| 51 |
|
| 52 |
/* ---- Login ---- */
|
| 53 |
-
#login
|
| 54 |
-
.login-
|
| 55 |
-
.login-
|
| 56 |
-
.login-
|
| 57 |
-
.login-
|
| 58 |
-
.login-
|
| 59 |
-
.login-
|
| 60 |
|
| 61 |
/* ---- Layout ---- */
|
| 62 |
#app{display:none;min-height:100vh}
|
| 63 |
-
.
|
| 64 |
-
.
|
| 65 |
-
.
|
| 66 |
-
.
|
| 67 |
-
.nav
|
| 68 |
-
.nav
|
| 69 |
-
.nav
|
| 70 |
-
.nav
|
| 71 |
-
.
|
| 72 |
-
.main{margin-left:
|
| 73 |
.main h2{font-size:1.3rem;margin-bottom:1.2rem}
|
| 74 |
|
| 75 |
/* ---- Mobile ---- */
|
| 76 |
-
.menu-
|
| 77 |
-
.
|
| 78 |
@media(max-width:768px){
|
| 79 |
-
.
|
| 80 |
-
.
|
| 81 |
-
.
|
| 82 |
-
.menu-
|
| 83 |
.main{margin-left:0;padding:1rem;padding-top:3.5rem}
|
| 84 |
}
|
| 85 |
|
| 86 |
-
/* ---- Cards ---- */
|
| 87 |
-
.stat-
|
| 88 |
-
.stat-
|
| 89 |
-
.stat-
|
| 90 |
-
.stat-
|
| 91 |
|
| 92 |
/* ---- Table ---- */
|
| 93 |
-
.
|
| 94 |
table{width:100%;border-collapse:collapse;font-size:.88rem}
|
| 95 |
-
th{text-align:left;padding:.7rem .9rem;font-weight:600;color:var(--
|
| 96 |
-
td{padding:.
|
| 97 |
tr:last-child td{border-bottom:none}
|
| 98 |
-
|
|
|
|
| 99 |
|
| 100 |
/* ---- Toolbar ---- */
|
| 101 |
-
.
|
| 102 |
-
.
|
| 103 |
-
.
|
| 104 |
|
| 105 |
/* ---- Pagination ---- */
|
| 106 |
-
.
|
| 107 |
|
| 108 |
/* ---- Modal ---- */
|
| 109 |
-
.modal-
|
| 110 |
-
.modal-
|
| 111 |
-
.modal{background:var(--surface);border-radius:12px;padding:1.8rem;width:100%;max-width:440px;box-shadow:0 8px 30px rgba(0,0,0,.12)}
|
| 112 |
.modal h3{margin-bottom:1rem;font-size:1.1rem}
|
| 113 |
.modal label{display:block;font-weight:500;margin-bottom:.3rem;font-size:.85rem;margin-top:.8rem}
|
| 114 |
.modal label:first-of-type{margin-top:0}
|
| 115 |
.modal input{width:100%}
|
| 116 |
-
.modal-
|
| 117 |
|
| 118 |
/* ---- Import ---- */
|
| 119 |
-
.
|
| 120 |
-
.
|
| 121 |
-
.
|
| 122 |
-
.
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
.
|
| 127 |
-
.
|
| 128 |
-
.
|
| 129 |
-
.
|
| 130 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
/* ---- Misc ---- */
|
| 133 |
-
.loading{text-align:center;padding:2rem;color:var(--
|
| 134 |
-
.empty{text-align:center;padding:2rem;color:var(--
|
| 135 |
-
|
| 136 |
</style>
|
| 137 |
</head>
|
| 138 |
<body>
|
| 139 |
|
| 140 |
-
<div id="toast-
|
| 141 |
|
| 142 |
-
<!-- LOGIN -->
|
| 143 |
-
<div id="login
|
| 144 |
-
<div class="login-
|
| 145 |
<h1>Outlook2API</h1>
|
| 146 |
<p>Sign in to the admin panel</p>
|
| 147 |
-
<form id="
|
| 148 |
-
<label for="
|
| 149 |
-
<input id="
|
| 150 |
-
<button type="submit" class="btn btn-
|
| 151 |
</form>
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
|
| 155 |
-
<!-- APP -->
|
| 156 |
<div id="app">
|
| 157 |
-
<button class="menu-
|
| 158 |
-
|
| 159 |
-
<
|
| 160 |
-
<
|
| 161 |
-
<div class="
|
| 162 |
-
<
|
| 163 |
-
<
|
| 164 |
-
<button class="nav-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
<
|
| 168 |
-
|
| 169 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>
|
| 170 |
-
Accounts
|
| 171 |
-
</button>
|
| 172 |
-
<button class="nav-item" data-tab="import" aria-label="Import">
|
| 173 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
| 174 |
-
Import
|
| 175 |
-
</button>
|
| 176 |
-
<button class="nav-item" data-tab="mailbox" aria-label="Mailbox">
|
| 177 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
| 178 |
-
Mailbox
|
| 179 |
-
</button>
|
| 180 |
-
<button class="nav-item" data-tab="docs" aria-label="API Docs">
|
| 181 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
| 182 |
-
API Docs
|
| 183 |
-
</button>
|
| 184 |
-
<div class="sidebar-footer">
|
| 185 |
-
<button class="nav-item" id="logout-btn" aria-label="Logout">
|
| 186 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
| 187 |
-
Logout
|
| 188 |
-
</button>
|
| 189 |
</div>
|
| 190 |
</nav>
|
| 191 |
|
| 192 |
<main class="main">
|
| 193 |
-
|
| 194 |
-
<
|
|
|
|
| 195 |
<h2>Dashboard</h2>
|
| 196 |
-
<div class="stat-
|
| 197 |
-
<div class="stat-
|
| 198 |
-
<div class="stat-
|
| 199 |
-
<div class="stat-
|
| 200 |
-
<div class="stat-
|
| 201 |
</div>
|
| 202 |
-
<button class="btn btn-
|
| 203 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
| 204 |
-
Export Accounts
|
| 205 |
-
</button>
|
| 206 |
</section>
|
| 207 |
|
| 208 |
-
<!-- Accounts -->
|
| 209 |
-
<section id="
|
| 210 |
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.5rem;margin-bottom:1.2rem">
|
| 211 |
<h2 style="margin-bottom:0">Accounts</h2>
|
| 212 |
<div style="display:flex;gap:.5rem">
|
| 213 |
-
<button class="btn btn-
|
| 214 |
-
<button class="btn btn-
|
| 215 |
</div>
|
| 216 |
</div>
|
| 217 |
-
<div class="
|
| 218 |
-
<input type="text" id="
|
| 219 |
-
<select id="
|
| 220 |
-
<option value="">All</option>
|
| 221 |
-
<option value="true">Active</option>
|
| 222 |
-
<option value="false">Inactive</option>
|
| 223 |
-
</select>
|
| 224 |
</div>
|
| 225 |
-
<div class="
|
| 226 |
-
<table>
|
| 227 |
-
|
| 228 |
-
<tbody id="accounts-tbody"><tr><td colspan="5" class="loading">Loading...</td></tr></tbody>
|
| 229 |
-
</table>
|
| 230 |
</div>
|
| 231 |
-
<div class="
|
| 232 |
-
<button class="btn btn-
|
| 233 |
-
<span id="
|
| 234 |
-
<button class="btn btn-
|
| 235 |
</div>
|
| 236 |
</section>
|
| 237 |
|
| 238 |
-
<!-- Import -->
|
| 239 |
-
<section id="
|
| 240 |
<h2>Import Accounts</h2>
|
| 241 |
-
<div class="
|
| 242 |
<h3>Text Import</h3>
|
| 243 |
<p>Enter accounts, one per line in <code>email:password</code> format.</p>
|
| 244 |
-
<textarea id="
|
| 245 |
-
<button class="btn btn-
|
| 246 |
</div>
|
| 247 |
-
<div class="
|
| 248 |
<h3>File Upload</h3>
|
| 249 |
<p>Upload a <code>.txt</code> file with one <code>email:password</code> per line.</p>
|
| 250 |
-
<input type="file" id="
|
| 251 |
-
<div
|
| 252 |
</div>
|
| 253 |
-
<div class="
|
| 254 |
<h3>CI Import</h3>
|
| 255 |
-
<p>Use this endpoint in
|
| 256 |
-
<pre>POST /admin/api/accounts/bulk
|
| 257 |
-
Content-Type: application/json
|
| 258 |
-
Authorization: Bearer ADMIN_PASSWORD
|
| 259 |
-
|
| 260 |
-
{
|
| 261 |
-
"accounts": ["user@outlook.com:pass"],
|
| 262 |
-
"source": "ci"
|
| 263 |
-
}</pre>
|
| 264 |
-
<p style="margin-top:.6rem;font-weight:500">curl example:</p>
|
| 265 |
<pre>curl -X POST https://your-domain/admin/api/accounts/bulk \
|
| 266 |
-H "Authorization: Bearer ADMIN_PASSWORD" \
|
| 267 |
-H "Content-Type: application/json" \
|
| 268 |
-
-d '{"accounts":
|
| 269 |
</div>
|
| 270 |
</section>
|
| 271 |
|
| 272 |
-
<!--
|
| 273 |
-
<section id="
|
| 274 |
-
|
| 275 |
-
<!--
|
| 276 |
-
<
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
<
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
<div id="mailbox-list">
|
| 286 |
-
<div class="empty">Select an account and click "Load Messages"</div>
|
| 287 |
-
</div>
|
| 288 |
-
<div id="mailbox-detail" style="display:none;margin-top:1rem">
|
| 289 |
-
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.8rem">
|
| 290 |
-
<h3 id="mailbox-detail-subject" style="font-size:1rem;margin:0">Subject</h3>
|
| 291 |
-
<button class="btn btn-outline btn-sm" id="mailbox-back-btn">Back to list</button>
|
| 292 |
</div>
|
| 293 |
-
<
|
| 294 |
-
|
| 295 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
</div>
|
| 297 |
-
<
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
</div>
|
| 300 |
</div>
|
| 301 |
</section>
|
|
|
|
|
|
|
|
|
|
| 302 |
<h2>API Documentation</h2>
|
| 303 |
-
<div
|
| 304 |
-
|
| 305 |
-
<div class="endpoint">
|
| 306 |
-
<span class="method method-get">GET</span><span class="path">/domains</span>
|
| 307 |
-
<div class="desc">List supported email domains</div>
|
| 308 |
-
<pre>curl https://your-domain/domains</pre>
|
| 309 |
-
</div>
|
| 310 |
-
<div class="endpoint">
|
| 311 |
-
<span class="method method-post">POST</span><span class="path">/accounts</span>
|
| 312 |
-
<div class="desc">Register account (validates via IMAP)</div>
|
| 313 |
-
<pre>curl -X POST https://your-domain/accounts \
|
| 314 |
-
-H "Content-Type: application/json" \
|
| 315 |
-
-d '{"address": "user@outlook.com", "password": "pass"}'</pre>
|
| 316 |
-
</div>
|
| 317 |
-
<div class="endpoint">
|
| 318 |
-
<span class="method method-post">POST</span><span class="path">/token</span>
|
| 319 |
-
<div class="desc">Get JWT token</div>
|
| 320 |
-
<pre>curl -X POST https://your-domain/token \
|
| 321 |
-
-H "Content-Type: application/json" \
|
| 322 |
-
-d '{"address": "user@outlook.com", "password": "pass"}'</pre>
|
| 323 |
-
</div>
|
| 324 |
-
<div class="endpoint">
|
| 325 |
-
<span class="method method-get">GET</span><span class="path">/me</span>
|
| 326 |
-
<div class="desc">Get current user info (requires Bearer token)</div>
|
| 327 |
-
<pre>curl https://your-domain/me \
|
| 328 |
-
-H "Authorization: Bearer YOUR_JWT_TOKEN"</pre>
|
| 329 |
-
</div>
|
| 330 |
-
<div class="endpoint">
|
| 331 |
-
<span class="method method-get">GET</span><span class="path">/messages</span>
|
| 332 |
-
<div class="desc">List messages (requires Bearer token, ?page=1)</div>
|
| 333 |
-
<pre>curl "https://your-domain/messages?page=1" \
|
| 334 |
-
-H "Authorization: Bearer YOUR_JWT_TOKEN"</pre>
|
| 335 |
-
</div>
|
| 336 |
-
<div class="endpoint">
|
| 337 |
-
<span class="method method-get">GET</span><span class="path">/messages/{id}</span>
|
| 338 |
-
<div class="desc">Get a specific message (requires Bearer token)</div>
|
| 339 |
-
<pre>curl https://your-domain/messages/123 \
|
| 340 |
-
-H "Authorization: Bearer YOUR_JWT_TOKEN"</pre>
|
| 341 |
-
</div>
|
| 342 |
-
<div class="endpoint">
|
| 343 |
-
<span class="method method-get">GET</span><span class="path">/messages/{id}/code</span>
|
| 344 |
-
<div class="desc">Extract verification code from message (requires Bearer token)</div>
|
| 345 |
-
<pre>curl https://your-domain/messages/123/code \
|
| 346 |
-
-H "Authorization: Bearer YOUR_JWT_TOKEN"</pre>
|
| 347 |
-
</div>
|
| 348 |
-
<div class="endpoint">
|
| 349 |
-
<span class="method method-delete">DELETE</span><span class="path">/accounts/me</span>
|
| 350 |
-
<div class="desc">Delete current account (requires Bearer token)</div>
|
| 351 |
-
<pre>curl -X DELETE https://your-domain/accounts/me \
|
| 352 |
-
-H "Authorization: Bearer YOUR_JWT_TOKEN"</pre>
|
| 353 |
-
</div>
|
| 354 |
-
</div>
|
| 355 |
-
<div class="endpoint-group">
|
| 356 |
-
<h3>Admin API</h3>
|
| 357 |
-
<p style="color:var(--text-sec);font-size:.84rem;margin-bottom:.8rem;margin-top:-.4rem">All endpoints require Bearer token or admin_token cookie.</p>
|
| 358 |
-
<div class="endpoint">
|
| 359 |
-
<span class="method method-post">POST</span><span class="path">/admin/api/login</span>
|
| 360 |
-
<div class="desc">Authenticate and receive admin token</div>
|
| 361 |
-
<pre>curl -X POST https://your-domain/admin/api/login \
|
| 362 |
-
-H "Content-Type: application/json" \
|
| 363 |
-
-d '{"password": "your_password"}'</pre>
|
| 364 |
-
</div>
|
| 365 |
-
<div class="endpoint">
|
| 366 |
-
<span class="method method-get">GET</span><span class="path">/admin/api/stats</span>
|
| 367 |
-
<div class="desc">Get account statistics</div>
|
| 368 |
-
<pre>curl https://your-domain/admin/api/stats \
|
| 369 |
-
-H "Authorization: Bearer ADMIN_TOKEN"</pre>
|
| 370 |
-
</div>
|
| 371 |
-
<div class="endpoint">
|
| 372 |
-
<span class="method method-get">GET</span><span class="path">/admin/api/accounts</span>
|
| 373 |
-
<div class="desc">List accounts with pagination and filters (?page=&search=&active=)</div>
|
| 374 |
-
<pre>curl "https://your-domain/admin/api/accounts?page=1&search=user&active=true" \
|
| 375 |
-
-H "Authorization: Bearer ADMIN_TOKEN"</pre>
|
| 376 |
-
</div>
|
| 377 |
-
<div class="endpoint">
|
| 378 |
-
<span class="method method-post">POST</span><span class="path">/admin/api/accounts</span>
|
| 379 |
-
<div class="desc">Add a single account</div>
|
| 380 |
-
<pre>curl -X POST https://your-domain/admin/api/accounts \
|
| 381 |
-
-H "Authorization: Bearer ADMIN_TOKEN" \
|
| 382 |
-
-H "Content-Type: application/json" \
|
| 383 |
-
-d '{"email": "user@outlook.com", "password": "pass"}'</pre>
|
| 384 |
-
</div>
|
| 385 |
-
<div class="endpoint">
|
| 386 |
-
<span class="method method-post">POST</span><span class="path">/admin/api/accounts/bulk</span>
|
| 387 |
-
<div class="desc">Bulk import accounts</div>
|
| 388 |
-
<pre>curl -X POST https://your-domain/admin/api/accounts/bulk \
|
| 389 |
-
-H "Authorization: Bearer ADMIN_TOKEN" \
|
| 390 |
-
-H "Content-Type: application/json" \
|
| 391 |
-
-d '{"accounts": ["user@outlook.com:pass"], "source": "manual"}'</pre>
|
| 392 |
-
</div>
|
| 393 |
-
<div class="endpoint">
|
| 394 |
-
<span class="method method-post">POST</span><span class="path">/admin/api/accounts/upload</span>
|
| 395 |
-
<div class="desc">Upload accounts file (multipart form)</div>
|
| 396 |
-
<pre>curl -X POST https://your-domain/admin/api/accounts/upload \
|
| 397 |
-
-H "Authorization: Bearer ADMIN_TOKEN" \
|
| 398 |
-
-F "file=@accounts.txt"</pre>
|
| 399 |
-
</div>
|
| 400 |
-
<div class="endpoint">
|
| 401 |
-
<span class="method method-patch">PATCH</span><span class="path">/admin/api/accounts/{id}</span>
|
| 402 |
-
<div class="desc">Update account (toggle active status)</div>
|
| 403 |
-
<pre>curl -X PATCH https://your-domain/admin/api/accounts/123 \
|
| 404 |
-
-H "Authorization: Bearer ADMIN_TOKEN" \
|
| 405 |
-
-H "Content-Type: application/json" \
|
| 406 |
-
-d '{"is_active": true}'</pre>
|
| 407 |
-
</div>
|
| 408 |
-
<div class="endpoint">
|
| 409 |
-
<span class="method method-delete">DELETE</span><span class="path">/admin/api/accounts/{id}</span>
|
| 410 |
-
<div class="desc">Delete a specific account</div>
|
| 411 |
-
<pre>curl -X DELETE https://your-domain/admin/api/accounts/123 \
|
| 412 |
-
-H "Authorization: Bearer ADMIN_TOKEN"</pre>
|
| 413 |
-
</div>
|
| 414 |
-
<div class="endpoint">
|
| 415 |
-
<span class="method method-delete">DELETE</span><span class="path">/admin/api/accounts</span>
|
| 416 |
-
<div class="desc">Delete all accounts</div>
|
| 417 |
-
<pre>curl -X DELETE https://your-domain/admin/api/accounts \
|
| 418 |
-
-H "Authorization: Bearer ADMIN_TOKEN"</pre>
|
| 419 |
-
</div>
|
| 420 |
-
<div class="endpoint">
|
| 421 |
-
<span class="method method-get">GET</span><span class="path">/admin/api/accounts/{id}/password</span>
|
| 422 |
-
<div class="desc">Retrieve account password</div>
|
| 423 |
-
<pre>curl https://your-domain/admin/api/accounts/123/password \
|
| 424 |
-
-H "Authorization: Bearer ADMIN_TOKEN"</pre>
|
| 425 |
-
</div>
|
| 426 |
-
<div class="endpoint">
|
| 427 |
-
<span class="method method-get">GET</span><span class="path">/admin/api/export</span>
|
| 428 |
-
<div class="desc">Export all accounts as text file</div>
|
| 429 |
-
<pre>curl https://your-domain/admin/api/export \
|
| 430 |
-
-H "Authorization: Bearer ADMIN_TOKEN" -o accounts.txt</pre>
|
| 431 |
-
</div>
|
| 432 |
-
</div>
|
| 433 |
</section>
|
|
|
|
| 434 |
</main>
|
| 435 |
</div>
|
| 436 |
</div>
|
| 437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
<script>
|
| 439 |
(function(){
|
| 440 |
'use strict';
|
| 441 |
|
| 442 |
-
/
|
| 443 |
-
|
| 444 |
-
function
|
| 445 |
-
function
|
| 446 |
-
function
|
| 447 |
-
|
| 448 |
-
function
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
| 457 |
}
|
| 458 |
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
|
|
|
| 463 |
if(opts.body&&typeof opts.body==='object'&&!(opts.body instanceof FormData)){
|
| 464 |
headers['Content-Type']='application/json';
|
| 465 |
opts.body=JSON.stringify(opts.body);
|
| 466 |
}
|
| 467 |
opts.headers=headers;
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
|
|
|
| 477 |
}
|
| 478 |
|
| 479 |
-
/
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
let accountsTotal=0;
|
| 483 |
-
const PAGE_SIZE=20;
|
| 484 |
-
|
| 485 |
-
// ---- DOM refs ----
|
| 486 |
-
const $=id=>document.getElementById(id);
|
| 487 |
|
| 488 |
-
/
|
| 489 |
function checkAuth(){
|
| 490 |
-
if(
|
| 491 |
-
|
| 492 |
-
$('app').style.display='block';
|
| 493 |
-
loadTab(currentTab);
|
| 494 |
-
} else {
|
| 495 |
-
$('login-screen').style.display='flex';
|
| 496 |
-
$('app').style.display='none';
|
| 497 |
-
}
|
| 498 |
}
|
| 499 |
|
| 500 |
-
|
| 501 |
-
$('login-form').addEventListener('submit',async e=>{
|
| 502 |
e.preventDefault();
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
checkAuth();
|
| 510 |
-
}catch(err){toast(err.message,'error')}
|
| 511 |
-
finally{btn.disabled=false;btn.textContent='Sign in'}
|
| 512 |
-
});
|
| 513 |
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
loadTab(
|
| 524 |
-
// close mobile
|
| 525 |
-
$('
|
| 526 |
-
$('
|
| 527 |
-
}
|
| 528 |
});
|
| 529 |
|
| 530 |
-
|
| 531 |
-
$('
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
function loadTab(
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
if(
|
| 539 |
-
if(
|
| 540 |
-
if(
|
| 541 |
-
if(
|
| 542 |
}
|
| 543 |
|
| 544 |
-
/
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
$('
|
| 549 |
-
$('
|
| 550 |
-
$('
|
| 551 |
-
|
| 552 |
-
}catch(err){toast(err.message,'error')}
|
| 553 |
}
|
| 554 |
|
| 555 |
-
$('
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
a.href=url;a.download='accounts_export.txt';a.click();
|
| 564 |
URL.revokeObjectURL(url);
|
| 565 |
toast('Export downloaded');
|
| 566 |
-
}catch(
|
| 567 |
-
}
|
| 568 |
|
| 569 |
-
/
|
| 570 |
-
|
| 571 |
-
$('
|
| 572 |
-
$('
|
| 573 |
-
$('
|
| 574 |
-
$('
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
<button class="btn btn-outline btn-sm" onclick="window._showPassword('${esc(a.id)}')" aria-label="Show password">Password</button>
|
| 604 |
-
<button class="btn btn-outline btn-sm" onclick="window._openMailbox('${esc(a.id)}','${esc(a.email||'')}')" aria-label="Open mailbox">Mailbox</button>
|
| 605 |
-
<button class="btn btn-danger btn-sm" onclick="window._deleteAccount('${esc(a.id)}')" aria-label="Delete account">Delete</button>
|
| 606 |
-
</td>
|
| 607 |
-
</tr>`).join('');
|
| 608 |
-
}catch(err){
|
| 609 |
-
tbody.innerHTML='<tr><td colspan="5" class="empty">Error: '+esc(err.message)+'</td></tr>';
|
| 610 |
-
}
|
| 611 |
}
|
| 612 |
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
catch(err){toast(err.message,'error')}
|
| 619 |
};
|
| 620 |
-
|
| 621 |
-
$('
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
|
|
|
| 625 |
};
|
| 626 |
-
|
| 627 |
if(!confirm('Delete this account?'))return;
|
| 628 |
-
|
| 629 |
-
catch(
|
| 630 |
};
|
| 631 |
|
| 632 |
-
|
| 633 |
-
$('
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
$('
|
| 638 |
-
$('
|
| 639 |
-
$('
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
if(!
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
});
|
| 650 |
|
| 651 |
-
//
|
| 652 |
-
$('
|
| 653 |
-
if(!confirm('Delete ALL accounts?
|
| 654 |
if(!confirm('Are you really sure?'))return;
|
| 655 |
-
|
| 656 |
-
catch(
|
| 657 |
-
}
|
| 658 |
|
| 659 |
-
/
|
| 660 |
-
$('
|
| 661 |
-
|
| 662 |
-
if(!lines.length){toast('Enter at least one account',
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
finally{btn.disabled=false;btn.textContent='Import Accounts'}
|
| 670 |
-
});
|
| 671 |
|
| 672 |
-
|
| 673 |
-
$('
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
}catch(err){toast(err.message,'error')}
|
| 683 |
-
finally{btn.disabled=false;btn.textContent='Upload File'}
|
| 684 |
-
});
|
| 685 |
|
| 686 |
-
/
|
| 687 |
-
document.
|
| 688 |
-
if(e.key==='Escape'){
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
|
|
|
| 708 |
});
|
| 709 |
-
|
|
|
|
| 710 |
}
|
| 711 |
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
}
|
| 736 |
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
if(!m)return;
|
| 740 |
-
$('mailbox-list').style.display='none';
|
| 741 |
-
$('mailbox-detail').style.display='block';
|
| 742 |
-
$('mailbox-detail-subject').textContent=m.subject||'(no subject)';
|
| 743 |
-
$('mailbox-detail-from').textContent='From: '+((m.from&&m.from.address)||m.from||'');
|
| 744 |
-
$('mailbox-detail-code').textContent=m.verification_code?'Code: '+m.verification_code:'';
|
| 745 |
-
const iframe=$('mailbox-detail-body');
|
| 746 |
-
const html=(m.html&&m.html[0])||m.text||'(empty)';
|
| 747 |
-
iframe.srcdoc=html;
|
| 748 |
};
|
| 749 |
|
| 750 |
-
|
| 751 |
-
$('
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
$('
|
| 755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
loadMailboxMessages();
|
| 764 |
-
},300);
|
| 765 |
};
|
| 766 |
|
| 767 |
-
/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
checkAuth();
|
|
|
|
| 769 |
})();
|
| 770 |
</script>
|
| 771 |
-
|
| 772 |
-
<!-- Add Account Modal -->
|
| 773 |
-
<div class="modal-overlay" id="add-modal" aria-modal="true" role="dialog" aria-label="Add account">
|
| 774 |
-
<div class="modal">
|
| 775 |
-
<h3>Add Account</h3>
|
| 776 |
-
<label for="add-email">Email</label>
|
| 777 |
-
<input id="add-email" type="email" placeholder="user@outlook.com" required>
|
| 778 |
-
<label for="add-password">Password</label>
|
| 779 |
-
<input id="add-password" type="text" placeholder="Password" required>
|
| 780 |
-
<div class="modal-actions">
|
| 781 |
-
<button class="btn btn-outline" id="add-cancel-btn">Cancel</button>
|
| 782 |
-
<button class="btn btn-primary" id="add-confirm-btn">Add Account</button>
|
| 783 |
-
</div>
|
| 784 |
-
</div>
|
| 785 |
-
</div>
|
| 786 |
-
|
| 787 |
-
<!-- Password Modal -->
|
| 788 |
-
<div class="modal-overlay" id="pw-modal" aria-modal="true" role="dialog" aria-label="Account password">
|
| 789 |
-
<div class="modal">
|
| 790 |
-
<h3>Account Password</h3>
|
| 791 |
-
<p id="pw-display" style="font-family:monospace;background:var(--bg);padding:.7rem;border-radius:6px;margin-top:.5rem;word-break:break-all">Loading...</p>
|
| 792 |
-
<div class="modal-actions"><button class="btn btn-outline" id="pw-close-btn">Close</button></div>
|
| 793 |
-
</div>
|
| 794 |
-
</div>
|
| 795 |
</body>
|
| 796 |
</html>
|
|
|
|
| 9 |
<style>
|
| 10 |
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 11 |
:root{
|
| 12 |
+
--bg:#f8f7f4;--surface:#fff;--border:#e8e5e0;
|
| 13 |
+
--text:#1a1a1a;--text2:#6b6560;--text3:#9b958e;
|
| 14 |
+
--brand:#c96442;--brand-h:#b5573a;--brand-bg:rgba(201,100,66,.07);
|
| 15 |
+
--ok:#2d8a4e;--ok-bg:rgba(45,138,78,.08);
|
| 16 |
+
--err:#dc3545;--err-bg:rgba(220,53,69,.06);
|
| 17 |
+
--info:#2563eb;--info-bg:rgba(37,99,235,.07);
|
| 18 |
+
--warn:#e65100;--warn-bg:rgba(230,81,0,.07);
|
| 19 |
+
--r:8px;--shadow:0 1px 3px rgba(0,0,0,.05);--shadow2:0 4px 12px rgba(0,0,0,.07);
|
| 20 |
}
|
| 21 |
html{font-family:'DM Sans',system-ui,-apple-system,sans-serif;font-size:15px;color:var(--text);background:var(--bg)}
|
| 22 |
body{min-height:100vh}
|
| 23 |
+
a{color:var(--brand);text-decoration:none}
|
| 24 |
+
input,textarea,select{font-family:inherit;font-size:.9rem;border:1px solid var(--border);border-radius:var(--r);padding:.55rem .75rem;background:var(--surface);color:var(--text);outline:none;transition:border .2s}
|
| 25 |
+
input:focus,textarea:focus,select:focus{border-color:var(--brand)}
|
|
|
|
| 26 |
textarea{resize:vertical}
|
| 27 |
+
button{font-family:inherit;cursor:pointer;border:none;background:none;font-size:.9rem}
|
| 28 |
|
| 29 |
/* ---- Buttons ---- */
|
| 30 |
+
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.5rem 1rem;border-radius:var(--r);font-weight:500;transition:all .15s;font-size:.85rem}
|
| 31 |
+
.btn-p{background:var(--brand);color:#fff}.btn-p:hover{background:var(--brand-h)}
|
| 32 |
+
.btn-o{border:1px solid var(--border);color:var(--text)}.btn-o:hover{border-color:var(--brand);color:var(--brand)}
|
| 33 |
+
.btn-d{background:var(--err);color:#fff}.btn-d:hover{background:#c82333}
|
| 34 |
+
.btn-s{padding:.3rem .6rem;font-size:.78rem}
|
| 35 |
+
.btn:disabled{opacity:.45;cursor:not-allowed}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
/* ---- Toast ---- */
|
| 38 |
+
#toast-box{position:fixed;top:1rem;right:1rem;z-index:9999;display:flex;flex-direction:column;gap:.5rem}
|
| 39 |
+
.toast{padding:.65rem 1rem;border-radius:var(--r);background:var(--surface);border:1px solid var(--border);box-shadow:var(--shadow2);font-size:.84rem;animation:tIn .25s ease;max-width:340px;border-left:3px solid var(--ok)}
|
| 40 |
+
.toast-err{border-left-color:var(--err)}
|
| 41 |
+
@keyframes tIn{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}
|
| 42 |
|
| 43 |
/* ---- Login ---- */
|
| 44 |
+
#login{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:1rem}
|
| 45 |
+
.login-box{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:2.5rem;width:100%;max-width:380px;box-shadow:var(--shadow2)}
|
| 46 |
+
.login-box h1{font-size:1.4rem;margin-bottom:.3rem}
|
| 47 |
+
.login-box p{color:var(--text2);font-size:.88rem;margin-bottom:1.5rem}
|
| 48 |
+
.login-box label{display:block;font-weight:500;margin-bottom:.35rem;font-size:.85rem}
|
| 49 |
+
.login-box input{width:100%;margin-bottom:1rem}
|
| 50 |
+
.login-box .btn{width:100%;justify-content:center}
|
| 51 |
|
| 52 |
/* ---- Layout ---- */
|
| 53 |
#app{display:none;min-height:100vh}
|
| 54 |
+
.lay{display:flex;min-height:100vh}
|
| 55 |
+
.side{width:220px;background:var(--surface);border-right:1px solid var(--border);padding:1.2rem 0;display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100;transition:transform .25s}
|
| 56 |
+
.side-brand{padding:0 1.2rem;font-size:1.1rem;font-weight:700;color:var(--brand);border-bottom:1px solid var(--border);margin-bottom:.5rem;padding-bottom:1rem}
|
| 57 |
+
.side-brand span{color:var(--text2);font-weight:400;font-size:.78rem;display:block;margin-top:.15rem}
|
| 58 |
+
.nav{display:flex;align-items:center;gap:.6rem;padding:.6rem 1.2rem;color:var(--text2);font-weight:500;font-size:.9rem;transition:all .15s;cursor:pointer;border:none;width:100%;text-align:left;background:none}
|
| 59 |
+
.nav:hover{color:var(--text);background:var(--brand-bg)}
|
| 60 |
+
.nav.on{color:var(--brand);background:var(--brand-bg)}
|
| 61 |
+
.nav svg{width:18px;height:18px;flex-shrink:0}
|
| 62 |
+
.side-foot{margin-top:auto;padding-top:.5rem;border-top:1px solid var(--border)}
|
| 63 |
+
.main{margin-left:220px;flex:1;padding:2rem;max-width:960px;width:100%}
|
| 64 |
.main h2{font-size:1.3rem;margin-bottom:1.2rem}
|
| 65 |
|
| 66 |
/* ---- Mobile ---- */
|
| 67 |
+
.menu-btn{display:none;position:fixed;top:.8rem;left:.8rem;z-index:200;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:.45rem .6rem}
|
| 68 |
+
.ov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.25);z-index:90}
|
| 69 |
@media(max-width:768px){
|
| 70 |
+
.side{transform:translateX(-100%)}
|
| 71 |
+
.side.open{transform:translateX(0)}
|
| 72 |
+
.ov.open{display:block}
|
| 73 |
+
.menu-btn{display:block}
|
| 74 |
.main{margin-left:0;padding:1rem;padding-top:3.5rem}
|
| 75 |
}
|
| 76 |
|
| 77 |
+
/* ---- Cards/Stats ---- */
|
| 78 |
+
.stat-g{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:1rem;margin-bottom:1.5rem}
|
| 79 |
+
.stat-c{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:1.2rem;box-shadow:var(--shadow)}
|
| 80 |
+
.stat-c .l{font-size:.76rem;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;font-weight:600}
|
| 81 |
+
.stat-c .v{font-size:1.8rem;font-weight:700;margin-top:.3rem}
|
| 82 |
|
| 83 |
/* ---- Table ---- */
|
| 84 |
+
.tbl-w{overflow-x:auto;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--shadow)}
|
| 85 |
table{width:100%;border-collapse:collapse;font-size:.88rem}
|
| 86 |
+
th{text-align:left;padding:.7rem .9rem;font-weight:600;color:var(--text2);font-size:.76rem;text-transform:uppercase;letter-spacing:.03em;border-bottom:1px solid var(--border);background:var(--bg)}
|
| 87 |
+
td{padding:.6rem .9rem;border-bottom:1px solid var(--border)}
|
| 88 |
tr:last-child td{border-bottom:none}
|
| 89 |
+
tr:hover td{background:rgba(0,0,0,.015)}
|
| 90 |
+
.acts{display:flex;gap:.3rem;flex-wrap:wrap}
|
| 91 |
|
| 92 |
/* ---- Toolbar ---- */
|
| 93 |
+
.bar{display:flex;gap:.6rem;flex-wrap:wrap;margin-bottom:1rem;align-items:center}
|
| 94 |
+
.bar input[type=text]{flex:1;min-width:180px}
|
| 95 |
+
.bar select{min-width:120px}
|
| 96 |
|
| 97 |
/* ---- Pagination ---- */
|
| 98 |
+
.pag{display:flex;align-items:center;gap:.8rem;justify-content:center;margin-top:1rem;font-size:.88rem;color:var(--text2)}
|
| 99 |
|
| 100 |
/* ---- Modal ---- */
|
| 101 |
+
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:500;align-items:center;justify-content:center;padding:1rem}
|
| 102 |
+
.modal{background:var(--surface);border-radius:12px;padding:1.8rem;width:100%;max-width:440px;box-shadow:var(--shadow2)}
|
|
|
|
| 103 |
.modal h3{margin-bottom:1rem;font-size:1.1rem}
|
| 104 |
.modal label{display:block;font-weight:500;margin-bottom:.3rem;font-size:.85rem;margin-top:.8rem}
|
| 105 |
.modal label:first-of-type{margin-top:0}
|
| 106 |
.modal input{width:100%}
|
| 107 |
+
.modal-foot{display:flex;gap:.5rem;justify-content:flex-end;margin-top:1.2rem}
|
| 108 |
|
| 109 |
/* ---- Import ---- */
|
| 110 |
+
.imp{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:1.4rem;margin-bottom:1.2rem;box-shadow:var(--shadow)}
|
| 111 |
+
.imp h3{font-size:1rem;margin-bottom:.6rem}
|
| 112 |
+
.imp p{color:var(--text2);font-size:.85rem;margin-bottom:.8rem}
|
| 113 |
+
.imp textarea{width:100%;min-height:120px;margin-bottom:.8rem}
|
| 114 |
+
.imp code{background:var(--bg);padding:.1rem .4rem;border-radius:4px;font-size:.82rem}
|
| 115 |
+
|
| 116 |
+
/* ---- Badges ---- */
|
| 117 |
+
.badge{display:inline-block;padding:.12rem .5rem;border-radius:99px;font-size:.73rem;font-weight:600}
|
| 118 |
+
.b-ok{background:var(--ok-bg);color:var(--ok)}
|
| 119 |
+
.b-err{background:var(--err-bg);color:var(--err)}
|
| 120 |
+
.meth{display:inline-block;padding:.1rem .4rem;border-radius:4px;font-size:.68rem;font-weight:700;font-family:monospace;min-width:48px;text-align:center}
|
| 121 |
+
.m-get{background:#e8f5e9;color:#2e7d32}
|
| 122 |
+
.m-post{background:#e3f2fd;color:#1565c0}
|
| 123 |
+
.m-patch{background:var(--warn-bg);color:var(--warn)}
|
| 124 |
+
.m-del{background:#fce4ec;color:#c62828}
|
| 125 |
+
|
| 126 |
+
/* ---- Mailbox 3-col ---- */
|
| 127 |
+
.mb-wrap{display:flex;height:calc(100vh - 0px)}
|
| 128 |
+
.mb-col1{width:240px;border-right:1px solid var(--border);background:var(--surface);display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
|
| 129 |
+
.mb-col2{width:300px;border-right:1px solid var(--border);background:var(--surface);display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
|
| 130 |
+
.mb-col3{flex:1;background:var(--bg);display:flex;flex-direction:column;min-width:0;overflow:hidden}
|
| 131 |
+
.mb-hdr{padding:.75rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:.5rem;flex-shrink:0}
|
| 132 |
+
.mb-list{flex:1;overflow-y:auto}
|
| 133 |
+
.mb-item{padding:.7rem .75rem;border-bottom:1px solid var(--border);cursor:pointer;transition:background .1s;font-size:.82rem}
|
| 134 |
+
.mb-item:hover{background:var(--brand-bg)}
|
| 135 |
+
.mb-item.on{background:var(--brand-bg)}
|
| 136 |
+
.mb-empty{text-align:center;padding:2rem;color:var(--text3);font-size:.84rem}
|
| 137 |
+
|
| 138 |
+
/* ---- Docs ---- */
|
| 139 |
+
.doc-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);margin-bottom:.6rem;overflow:hidden;box-shadow:var(--shadow)}
|
| 140 |
+
.doc-head{padding:.7rem 1rem;cursor:pointer;display:flex;align-items:center;gap:.7rem;transition:background .1s}
|
| 141 |
+
.doc-head:hover{background:var(--bg)}
|
| 142 |
+
.doc-body{border-top:1px solid var(--border);padding:1rem;display:none}
|
| 143 |
+
.doc-desc{color:var(--text2);font-size:.82rem;margin-bottom:.6rem}
|
| 144 |
+
.doc-tabs{display:flex;gap:.3rem;margin-bottom:.5rem}
|
| 145 |
+
.doc-tab{padding:.25rem .6rem;border-radius:4px;font-size:.78rem;font-weight:500;color:var(--text3);cursor:pointer;transition:all .1s}
|
| 146 |
+
.doc-tab.on{background:var(--bg);color:var(--text)}
|
| 147 |
+
.doc-pre{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.7rem;font-size:.76rem;overflow-x:auto;white-space:pre-wrap;word-break:break-all;font-family:monospace}
|
| 148 |
|
| 149 |
/* ---- Misc ---- */
|
| 150 |
+
.loading{text-align:center;padding:2rem;color:var(--text3)}
|
| 151 |
+
.empty{text-align:center;padding:2rem;color:var(--text3);font-size:.9rem}
|
| 152 |
+
pre{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.7rem;margin-top:.6rem;font-size:.78rem;overflow-x:auto;white-space:pre-wrap;word-break:break-all;font-family:monospace}
|
| 153 |
</style>
|
| 154 |
</head>
|
| 155 |
<body>
|
| 156 |
|
| 157 |
+
<div id="toast-box"></div>
|
| 158 |
|
| 159 |
+
<!-- ===== LOGIN ===== -->
|
| 160 |
+
<div id="login">
|
| 161 |
+
<div class="login-box">
|
| 162 |
<h1>Outlook2API</h1>
|
| 163 |
<p>Sign in to the admin panel</p>
|
| 164 |
+
<form id="loginForm">
|
| 165 |
+
<label for="loginPw">Password</label>
|
| 166 |
+
<input id="loginPw" type="password" placeholder="Admin password" required>
|
| 167 |
+
<button type="submit" class="btn btn-p" id="loginBtn">Sign in</button>
|
| 168 |
</form>
|
| 169 |
</div>
|
| 170 |
</div>
|
| 171 |
|
| 172 |
+
<!-- ===== APP ===== -->
|
| 173 |
<div id="app">
|
| 174 |
+
<button class="menu-btn" id="menuBtn"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h18M3 6h18M3 18h18"/></svg></button>
|
| 175 |
+
<div class="ov" id="ov"></div>
|
| 176 |
+
<div class="lay">
|
| 177 |
+
<nav class="side" id="side">
|
| 178 |
+
<div class="side-brand">Outlook2API<span>Admin Panel</span></div>
|
| 179 |
+
<button class="nav on" data-t="dash"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>Dashboard</button>
|
| 180 |
+
<button class="nav" data-t="acct"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>Accounts</button>
|
| 181 |
+
<button class="nav" data-t="imp"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>Import</button>
|
| 182 |
+
<button class="nav" data-t="mail"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>Mailbox</button>
|
| 183 |
+
<button class="nav" data-t="docs"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>API Docs</button>
|
| 184 |
+
<div class="side-foot">
|
| 185 |
+
<button class="nav" id="logoutBtn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>Logout</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
</div>
|
| 187 |
</nav>
|
| 188 |
|
| 189 |
<main class="main">
|
| 190 |
+
|
| 191 |
+
<!-- Tab: Dashboard -->
|
| 192 |
+
<section id="t-dash">
|
| 193 |
<h2>Dashboard</h2>
|
| 194 |
+
<div class="stat-g">
|
| 195 |
+
<div class="stat-c"><div class="l">Total Accounts</div><div class="v" id="sTotal">--</div></div>
|
| 196 |
+
<div class="stat-c"><div class="l">Active</div><div class="v" id="sActive">--</div></div>
|
| 197 |
+
<div class="stat-c"><div class="l">Inactive</div><div class="v" id="sInactive">--</div></div>
|
| 198 |
+
<div class="stat-c"><div class="l">Last 7 Days</div><div class="v" id="sRecent">--</div></div>
|
| 199 |
</div>
|
| 200 |
+
<button class="btn btn-o" id="exportBtn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>Export Accounts</button>
|
|
|
|
|
|
|
|
|
|
| 201 |
</section>
|
| 202 |
|
| 203 |
+
<!-- Tab: Accounts -->
|
| 204 |
+
<section id="t-acct" style="display:none">
|
| 205 |
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.5rem;margin-bottom:1.2rem">
|
| 206 |
<h2 style="margin-bottom:0">Accounts</h2>
|
| 207 |
<div style="display:flex;gap:.5rem">
|
| 208 |
+
<button class="btn btn-p btn-s" id="addBtn">+ Add</button>
|
| 209 |
+
<button class="btn btn-d btn-s" id="delAllBtn">Delete All</button>
|
| 210 |
</div>
|
| 211 |
</div>
|
| 212 |
+
<div class="bar">
|
| 213 |
+
<input type="text" id="searchIn" placeholder="Search by email...">
|
| 214 |
+
<select id="filterSel"><option value="">All</option><option value="true">Active</option><option value="false">Inactive</option></select>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
</div>
|
| 216 |
+
<div class="tbl-w">
|
| 217 |
+
<table><thead><tr><th>Email</th><th>Status</th><th>Source</th><th>Created</th><th>Actions</th></tr></thead>
|
| 218 |
+
<tbody id="acctBody"><tr><td colspan="5" class="loading">Loading...</td></tr></tbody></table>
|
|
|
|
|
|
|
| 219 |
</div>
|
| 220 |
+
<div class="pag">
|
| 221 |
+
<button class="btn btn-o btn-s" id="prevBtn" disabled>Prev</button>
|
| 222 |
+
<span id="pageInfo">Page 1</span>
|
| 223 |
+
<button class="btn btn-o btn-s" id="nextBtn" disabled>Next</button>
|
| 224 |
</div>
|
| 225 |
</section>
|
| 226 |
|
| 227 |
+
<!-- Tab: Import -->
|
| 228 |
+
<section id="t-imp" style="display:none">
|
| 229 |
<h2>Import Accounts</h2>
|
| 230 |
+
<div class="imp">
|
| 231 |
<h3>Text Import</h3>
|
| 232 |
<p>Enter accounts, one per line in <code>email:password</code> format.</p>
|
| 233 |
+
<textarea id="bulkText" placeholder="user1@outlook.com:password1 user2@outlook.com:password2"></textarea>
|
| 234 |
+
<button class="btn btn-p" id="bulkBtn">Import Accounts</button>
|
| 235 |
</div>
|
| 236 |
+
<div class="imp">
|
| 237 |
<h3>File Upload</h3>
|
| 238 |
<p>Upload a <code>.txt</code> file with one <code>email:password</code> per line.</p>
|
| 239 |
+
<input type="file" id="fileIn" accept=".txt" style="margin-bottom:.6rem">
|
| 240 |
+
<div><button class="btn btn-p" id="fileBtn">Upload File</button></div>
|
| 241 |
</div>
|
| 242 |
+
<div class="imp">
|
| 243 |
<h3>CI Import</h3>
|
| 244 |
+
<p>Use this endpoint in GitHub Actions to auto-import:</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
<pre>curl -X POST https://your-domain/admin/api/accounts/bulk \
|
| 246 |
-H "Authorization: Bearer ADMIN_PASSWORD" \
|
| 247 |
-H "Content-Type: application/json" \
|
| 248 |
+
-d '{"accounts":["user@outlook.com:pass"],"source":"ci"}'</pre>
|
| 249 |
</div>
|
| 250 |
</section>
|
| 251 |
|
| 252 |
+
<!-- Tab: Mailbox (three-column) -->
|
| 253 |
+
<section id="t-mail" style="display:none">
|
| 254 |
+
<div class="mb-wrap">
|
| 255 |
+
<!-- Col 1: Accounts -->
|
| 256 |
+
<div class="mb-col1">
|
| 257 |
+
<div class="mb-hdr">
|
| 258 |
+
<strong style="font-size:.85rem">Accounts</strong>
|
| 259 |
+
<button class="btn btn-p btn-s" id="composeBtn">Compose</button>
|
| 260 |
+
</div>
|
| 261 |
+
<div style="padding:.5rem;border-bottom:1px solid var(--border)">
|
| 262 |
+
<input type="text" id="mbSearch" placeholder="Search..." style="width:100%;padding:.4rem .6rem;font-size:.8rem">
|
| 263 |
+
</div>
|
| 264 |
+
<div class="mb-list" id="mbAcctList"><div class="mb-empty">Loading...</div></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
</div>
|
| 266 |
+
<!-- Col 2: Messages -->
|
| 267 |
+
<div class="mb-col2">
|
| 268 |
+
<div class="mb-hdr">
|
| 269 |
+
<strong id="mbInboxTitle" style="font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">Inbox</strong>
|
| 270 |
+
<button class="btn btn-o btn-s" id="mbRefresh" title="Refresh"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg></button>
|
| 271 |
+
</div>
|
| 272 |
+
<div class="mb-list" id="mbMsgList"><div class="mb-empty">Select an account</div></div>
|
| 273 |
</div>
|
| 274 |
+
<!-- Col 3: Detail/Compose -->
|
| 275 |
+
<div class="mb-col3">
|
| 276 |
+
<!-- Empty state -->
|
| 277 |
+
<div id="mbEmpty" style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--text3)">Select a message to read</div>
|
| 278 |
+
<!-- Detail view -->
|
| 279 |
+
<div id="mbDetail" style="display:none;flex:1;display:flex;flex-direction:column">
|
| 280 |
+
<div style="padding:1rem;border-bottom:1px solid var(--border);background:var(--surface)">
|
| 281 |
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.4rem">
|
| 282 |
+
<h3 id="mbSubj" style="font-size:1rem;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:1rem">Subject</h3>
|
| 283 |
+
<button class="btn btn-o btn-s" id="mbReplyBtn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 00-4-4H4"/></svg> Reply</button>
|
| 284 |
+
</div>
|
| 285 |
+
<div style="font-size:.82rem;color:var(--text2)">
|
| 286 |
+
<span id="mbFrom"></span>
|
| 287 |
+
<span id="mbCode" style="margin-left:.8rem"></span>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
<div style="flex:1;padding:1rem;overflow:auto">
|
| 291 |
+
<iframe id="mbBody" style="width:100%;height:100%;min-height:400px;border:1px solid var(--border);border-radius:var(--r);background:var(--surface)" sandbox="allow-same-origin"></iframe>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
<!-- Compose view -->
|
| 295 |
+
<div id="mbCompose" style="display:none;flex:1;display:flex;flex-direction:column">
|
| 296 |
+
<div style="padding:1rem;border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;justify-content:space-between">
|
| 297 |
+
<h3 style="font-size:1rem;margin:0">Compose Email</h3>
|
| 298 |
+
<button class="btn btn-o btn-s" id="composeCancel">Cancel</button>
|
| 299 |
+
</div>
|
| 300 |
+
<div style="flex:1;padding:1rem;overflow:auto">
|
| 301 |
+
<div style="max-width:600px">
|
| 302 |
+
<label style="display:block;font-size:.82rem;font-weight:500;color:var(--text2);margin-bottom:.3rem">From</label>
|
| 303 |
+
<select id="cFrom" style="width:100%;margin-bottom:.8rem"></select>
|
| 304 |
+
<label style="display:block;font-size:.82rem;font-weight:500;color:var(--text2);margin-bottom:.3rem">To</label>
|
| 305 |
+
<input id="cTo" type="email" placeholder="recipient@example.com" style="width:100%;margin-bottom:.8rem">
|
| 306 |
+
<label style="display:block;font-size:.82rem;font-weight:500;color:var(--text2);margin-bottom:.3rem">CC</label>
|
| 307 |
+
<input id="cCc" placeholder="cc@example.com (comma separated)" style="width:100%;margin-bottom:.8rem">
|
| 308 |
+
<label style="display:block;font-size:.82rem;font-weight:500;color:var(--text2);margin-bottom:.3rem">Subject</label>
|
| 309 |
+
<input id="cSubj" placeholder="Subject" style="width:100%;margin-bottom:.8rem">
|
| 310 |
+
<label style="display:block;font-size:.82rem;font-weight:500;color:var(--text2);margin-bottom:.3rem">Body</label>
|
| 311 |
+
<textarea id="cBody" rows="10" placeholder="Write your message..." style="width:100%;margin-bottom:1rem"></textarea>
|
| 312 |
+
<input type="hidden" id="cReplyTo"><input type="hidden" id="cRefs">
|
| 313 |
+
<div style="text-align:right">
|
| 314 |
+
<button class="btn btn-p" id="sendBtn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> Send Email</button>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
</div>
|
| 320 |
</div>
|
| 321 |
</section>
|
| 322 |
+
|
| 323 |
+
<!-- Tab: API Docs -->
|
| 324 |
+
<section id="t-docs" style="display:none">
|
| 325 |
<h2>API Documentation</h2>
|
| 326 |
+
<div id="docsMailBox"><h3 style="font-size:1rem;margin-bottom:.8rem;padding-bottom:.4rem;border-bottom:1px solid var(--border)">Mail API</h3></div>
|
| 327 |
+
<div id="docsAdminBox" style="margin-top:1.5rem"><h3 style="font-size:1rem;margin-bottom:.8rem;padding-bottom:.4rem;border-bottom:1px solid var(--border)">Admin API</h3><p style="color:var(--text2);font-size:.82rem;margin-bottom:.8rem">All endpoints require Bearer token or admin_token cookie.</p></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
</section>
|
| 329 |
+
|
| 330 |
</main>
|
| 331 |
</div>
|
| 332 |
</div>
|
| 333 |
|
| 334 |
+
<!-- ===== MODALS (BEFORE script!) ===== -->
|
| 335 |
+
<div class="modal-bg" id="addModal">
|
| 336 |
+
<div class="modal">
|
| 337 |
+
<h3>Add Account</h3>
|
| 338 |
+
<label>Email</label><input id="addEmail" type="email" placeholder="user@outlook.com">
|
| 339 |
+
<label>Password</label><input id="addPw" type="text" placeholder="Password">
|
| 340 |
+
<div class="modal-foot">
|
| 341 |
+
<button class="btn btn-o" id="addCancel">Cancel</button>
|
| 342 |
+
<button class="btn btn-p" id="addOk">Add Account</button>
|
| 343 |
+
</div>
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
|
| 347 |
+
<div class="modal-bg" id="pwModal">
|
| 348 |
+
<div class="modal">
|
| 349 |
+
<h3>Account Password</h3>
|
| 350 |
+
<p id="pwText" style="font-family:monospace;background:var(--bg);padding:.7rem;border-radius:6px;margin-top:.5rem;word-break:break-all">Loading...</p>
|
| 351 |
+
<div class="modal-foot"><button class="btn btn-o" id="pwClose">Close</button></div>
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
|
| 355 |
+
<!-- ===== SCRIPT ===== -->
|
| 356 |
<script>
|
| 357 |
(function(){
|
| 358 |
'use strict';
|
| 359 |
|
| 360 |
+
/* ========== HELPERS ========== */
|
| 361 |
+
var $=function(id){return document.getElementById(id)};
|
| 362 |
+
function getCk(n){var m=document.cookie.match(new RegExp('(?:^|; )'+n+'=([^;]*)'));return m?decodeURIComponent(m[1]):null}
|
| 363 |
+
function setCk(n,v,d){var s=n+'='+encodeURIComponent(v)+';path=/;SameSite=Lax';if(d)s+=';max-age='+d*86400;document.cookie=s}
|
| 364 |
+
function delCk(n){document.cookie=n+'=;path=/;max-age=0'}
|
| 365 |
+
function tk(){return getCk('admin_token')||''}
|
| 366 |
+
function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
| 367 |
+
function fmtD(s){if(!s)return'--';try{return new Date(s).toLocaleDateString()}catch(e){return s}}
|
| 368 |
+
|
| 369 |
+
// Show/hide — the ONLY way to toggle visibility. No CSS classes.
|
| 370 |
+
function show(id,disp){var el=typeof id==='string'?$(id):id;if(el)el.style.display=disp||''}
|
| 371 |
+
function hide(id){var el=typeof id==='string'?$(id):id;if(el)el.style.display='none'}
|
| 372 |
+
|
| 373 |
+
function toast(msg,err){
|
| 374 |
+
var c=$('toast-box'),d=document.createElement('div');
|
| 375 |
+
d.className='toast'+(err?' toast-err':'');
|
| 376 |
+
d.textContent=msg;c.appendChild(d);
|
| 377 |
+
setTimeout(function(){d.remove()},4000);
|
| 378 |
}
|
| 379 |
|
| 380 |
+
function api(path,opts){
|
| 381 |
+
opts=opts||{};
|
| 382 |
+
var headers=opts.headers||{};
|
| 383 |
+
var t=tk();
|
| 384 |
+
if(t)headers['Authorization']='Bearer '+t;
|
| 385 |
if(opts.body&&typeof opts.body==='object'&&!(opts.body instanceof FormData)){
|
| 386 |
headers['Content-Type']='application/json';
|
| 387 |
opts.body=JSON.stringify(opts.body);
|
| 388 |
}
|
| 389 |
opts.headers=headers;
|
| 390 |
+
return fetch(path,opts).then(function(r){
|
| 391 |
+
if(!r.ok){
|
| 392 |
+
return r.json().catch(function(){return{}}).then(function(j){
|
| 393 |
+
throw new Error(j.detail||j.message||'Request failed ('+r.status+')');
|
| 394 |
+
});
|
| 395 |
+
}
|
| 396 |
+
var ct=r.headers.get('content-type')||'';
|
| 397 |
+
if(ct.indexOf('json')>=0)return r.json();
|
| 398 |
+
return r;
|
| 399 |
+
});
|
| 400 |
}
|
| 401 |
|
| 402 |
+
/* ========== STATE ========== */
|
| 403 |
+
var curTab='dash',page=1,total=0,PS=20;
|
| 404 |
+
var mbMsgs=[],mbAccts=[],selAcctId=null,selAcctEmail='',curMsgIdx=-1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
|
| 406 |
+
/* ========== AUTH ========== */
|
| 407 |
function checkAuth(){
|
| 408 |
+
if(tk()){hide('login');show('app');loadTab(curTab)}
|
| 409 |
+
else{show('login','flex');hide('app')}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
}
|
| 411 |
|
| 412 |
+
$('loginForm').onsubmit=function(e){
|
|
|
|
| 413 |
e.preventDefault();
|
| 414 |
+
var btn=$('loginBtn');btn.disabled=true;btn.textContent='Signing in...';
|
| 415 |
+
api('/admin/api/login',{method:'POST',body:{password:$('loginPw').value}})
|
| 416 |
+
.then(function(d){setCk('admin_token',d.token||d.access_token,7);toast('Signed in');checkAuth()})
|
| 417 |
+
.catch(function(e){toast(e.message,1)})
|
| 418 |
+
.finally(function(){btn.disabled=false;btn.textContent='Sign in'});
|
| 419 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
|
| 421 |
+
$('logoutBtn').onclick=function(){delCk('admin_token');checkAuth()};
|
| 422 |
+
|
| 423 |
+
/* ========== SIDEBAR NAV ========== */
|
| 424 |
+
var navBtns=document.querySelectorAll('.nav[data-t]');
|
| 425 |
+
navBtns.forEach(function(b){
|
| 426 |
+
b.onclick=function(){
|
| 427 |
+
navBtns.forEach(function(x){x.classList.remove('on')});
|
| 428 |
+
b.classList.add('on');
|
| 429 |
+
curTab=b.getAttribute('data-t');
|
| 430 |
+
loadTab(curTab);
|
| 431 |
+
// close mobile
|
| 432 |
+
$('side').classList.remove('open');
|
| 433 |
+
$('ov').classList.remove('open');
|
| 434 |
+
};
|
| 435 |
});
|
| 436 |
|
| 437 |
+
$('menuBtn').onclick=function(){$('side').classList.toggle('open');$('ov').classList.toggle('open')};
|
| 438 |
+
$('ov').onclick=function(){$('side').classList.remove('open');$('ov').classList.remove('open')};
|
| 439 |
+
|
| 440 |
+
/* ========== TAB LOADER ========== */
|
| 441 |
+
var tabs=['dash','acct','imp','mail','docs'];
|
| 442 |
+
function loadTab(t){
|
| 443 |
+
tabs.forEach(function(id){hide('t-'+id)});
|
| 444 |
+
show('t-'+t);
|
| 445 |
+
if(t==='dash')loadDash();
|
| 446 |
+
if(t==='acct'){page=1;loadAccts()}
|
| 447 |
+
if(t==='mail')loadMbAccts();
|
| 448 |
+
if(t==='docs')renderDocs();
|
| 449 |
}
|
| 450 |
|
| 451 |
+
/* ========== DASHBOARD ========== */
|
| 452 |
+
function loadDash(){
|
| 453 |
+
api('/admin/api/stats').then(function(s){
|
| 454 |
+
$('sTotal').textContent=s.total!=null?s.total:'--';
|
| 455 |
+
$('sActive').textContent=s.active!=null?s.active:'--';
|
| 456 |
+
$('sInactive').textContent=s.inactive!=null?s.inactive:'--';
|
| 457 |
+
$('sRecent').textContent=s.recent_7d!=null?s.recent_7d:(s.recent!=null?s.recent:'--');
|
| 458 |
+
}).catch(function(e){toast(e.message,1)});
|
|
|
|
| 459 |
}
|
| 460 |
|
| 461 |
+
$('exportBtn').onclick=function(){
|
| 462 |
+
var t=tk();
|
| 463 |
+
fetch('/admin/api/export',{headers:{'Authorization':'Bearer '+t}}).then(function(r){
|
| 464 |
+
if(!r.ok)throw new Error('Export failed');
|
| 465 |
+
return r.text();
|
| 466 |
+
}).then(function(text){
|
| 467 |
+
var blob=new Blob([text],{type:'text/plain'});
|
| 468 |
+
var url=URL.createObjectURL(blob);
|
| 469 |
+
var a=document.createElement('a');a.href=url;a.download='accounts_export.txt';a.click();
|
| 470 |
URL.revokeObjectURL(url);
|
| 471 |
toast('Export downloaded');
|
| 472 |
+
}).catch(function(e){toast(e.message,1)});
|
| 473 |
+
};
|
| 474 |
|
| 475 |
+
/* ========== ACCOUNTS ========== */
|
| 476 |
+
var sTimer=null;
|
| 477 |
+
$('searchIn').oninput=function(){clearTimeout(sTimer);sTimer=setTimeout(function(){page=1;loadAccts()},350)};
|
| 478 |
+
$('filterSel').onchange=function(){page=1;loadAccts()};
|
| 479 |
+
$('prevBtn').onclick=function(){if(page>1){page--;loadAccts()}};
|
| 480 |
+
$('nextBtn').onclick=function(){page++;loadAccts()};
|
| 481 |
+
|
| 482 |
+
function loadAccts(){
|
| 483 |
+
var tb=$('acctBody');
|
| 484 |
+
tb.innerHTML='<tr><td colspan="5" class="loading">Loading...</td></tr>';
|
| 485 |
+
var q='/admin/api/accounts?page='+page+'&limit='+PS;
|
| 486 |
+
var s=$('searchIn').value;if(s)q+='&search='+encodeURIComponent(s);
|
| 487 |
+
var f=$('filterSel').value;if(f)q+='&active='+f;
|
| 488 |
+
api(q).then(function(d){
|
| 489 |
+
var a=d.accounts||[];total=d.total||a.length;
|
| 490 |
+
var tp=Math.max(1,Math.ceil(total/PS));
|
| 491 |
+
$('pageInfo').textContent='Page '+page+' of '+tp;
|
| 492 |
+
$('prevBtn').disabled=page<=1;$('nextBtn').disabled=page>=tp;
|
| 493 |
+
if(!a.length){tb.innerHTML='<tr><td colspan="5" class="empty">No accounts found</td></tr>';return}
|
| 494 |
+
tb.innerHTML=a.map(function(x){
|
| 495 |
+
return '<tr><td>'+esc(x.email||x.address||'')+'</td>'
|
| 496 |
+
+'<td><span class="badge '+(x.is_active!==false?'b-ok':'b-err')+'">'+(x.is_active!==false?'Active':'Inactive')+'</span></td>'
|
| 497 |
+
+'<td>'+esc(x.source||'--')+'</td>'
|
| 498 |
+
+'<td>'+fmtD(x.created_at||x.created||'')+'</td>'
|
| 499 |
+
+'<td><div class="acts">'
|
| 500 |
+
+'<button class="btn btn-o btn-s" onclick="W._tog(\''+esc(x.id)+'\','+(!x.is_active)+')">'+(x.is_active!==false?'Deactivate':'Activate')+'</button>'
|
| 501 |
+
+'<button class="btn btn-o btn-s" onclick="W._pw(\''+esc(x.id)+'\')">Password</button>'
|
| 502 |
+
+'<button class="btn btn-o btn-s" onclick="W._mb(\''+esc(x.id)+'\',\''+esc(x.email||'')+'\')">Mailbox</button>'
|
| 503 |
+
+'<button class="btn btn-d btn-s" onclick="W._del(\''+esc(x.id)+'\')">Delete</button>'
|
| 504 |
+
+'</div></td></tr>';
|
| 505 |
+
}).join('');
|
| 506 |
+
}).catch(function(e){
|
| 507 |
+
tb.innerHTML='<tr><td colspan="5" class="empty">Error: '+esc(e.message)+'</td></tr>';
|
| 508 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
}
|
| 510 |
|
| 511 |
+
// Global action handlers
|
| 512 |
+
var W=window;
|
| 513 |
+
W._tog=function(id,v){
|
| 514 |
+
api('/admin/api/accounts/'+id,{method:'PATCH',body:{is_active:v}})
|
| 515 |
+
.then(function(){toast('Account updated');loadAccts()}).catch(function(e){toast(e.message,1)});
|
|
|
|
| 516 |
};
|
| 517 |
+
W._pw=function(id){
|
| 518 |
+
$('pwText').textContent='Loading...';
|
| 519 |
+
show('pwModal','flex');
|
| 520 |
+
api('/admin/api/accounts/'+id+'/password')
|
| 521 |
+
.then(function(d){$('pwText').textContent=d.password||d.pw||JSON.stringify(d)})
|
| 522 |
+
.catch(function(e){$('pwText').textContent='Error: '+e.message});
|
| 523 |
};
|
| 524 |
+
W._del=function(id){
|
| 525 |
if(!confirm('Delete this account?'))return;
|
| 526 |
+
api('/admin/api/accounts/'+id,{method:'DELETE'})
|
| 527 |
+
.then(function(){toast('Account deleted');loadAccts()}).catch(function(e){toast(e.message,1)});
|
| 528 |
};
|
| 529 |
|
| 530 |
+
// Password modal close
|
| 531 |
+
$('pwClose').onclick=function(){hide('pwModal')};
|
| 532 |
+
$('pwModal').onclick=function(e){if(e.target===$('pwModal'))hide('pwModal')};
|
| 533 |
+
|
| 534 |
+
// Add account modal
|
| 535 |
+
$('addBtn').onclick=function(){$('addEmail').value='';$('addPw').value='';show('addModal','flex');$('addEmail').focus()};
|
| 536 |
+
$('addCancel').onclick=function(){hide('addModal')};
|
| 537 |
+
$('addModal').onclick=function(e){if(e.target===$('addModal'))hide('addModal')};
|
| 538 |
+
$('addOk').onclick=function(){
|
| 539 |
+
var em=$('addEmail').value.trim(),pw=$('addPw').value;
|
| 540 |
+
if(!em||!pw){toast('Fill in all fields',1);return}
|
| 541 |
+
var btn=$('addOk');btn.disabled=true;btn.textContent='Adding...';
|
| 542 |
+
api('/admin/api/accounts',{method:'POST',body:{email:em,password:pw}})
|
| 543 |
+
.then(function(){toast('Account added');hide('addModal');loadAccts()})
|
| 544 |
+
.catch(function(e){toast(e.message,1)})
|
| 545 |
+
.finally(function(){btn.disabled=false;btn.textContent='Add Account'});
|
| 546 |
+
};
|
|
|
|
| 547 |
|
| 548 |
+
// Delete all
|
| 549 |
+
$('delAllBtn').onclick=function(){
|
| 550 |
+
if(!confirm('Delete ALL accounts? Cannot be undone.'))return;
|
| 551 |
if(!confirm('Are you really sure?'))return;
|
| 552 |
+
api('/admin/api/accounts',{method:'DELETE'})
|
| 553 |
+
.then(function(){toast('All accounts deleted');loadAccts()}).catch(function(e){toast(e.message,1)});
|
| 554 |
+
};
|
| 555 |
|
| 556 |
+
/* ========== IMPORT ========== */
|
| 557 |
+
$('bulkBtn').onclick=function(){
|
| 558 |
+
var lines=$('bulkText').value.trim().split('\n').map(function(l){return l.trim()}).filter(Boolean);
|
| 559 |
+
if(!lines.length){toast('Enter at least one account',1);return}
|
| 560 |
+
var btn=$('bulkBtn');btn.disabled=true;btn.textContent='Importing...';
|
| 561 |
+
api('/admin/api/accounts/bulk',{method:'POST',body:{accounts:lines,source:'manual'}})
|
| 562 |
+
.then(function(d){toast('Imported '+(d.imported||0)+' accounts'+(d.skipped?' ('+d.skipped+' skipped)':''));$('bulkText').value=''})
|
| 563 |
+
.catch(function(e){toast(e.message,1)})
|
| 564 |
+
.finally(function(){btn.disabled=false;btn.textContent='Import Accounts'});
|
| 565 |
+
};
|
|
|
|
|
|
|
| 566 |
|
| 567 |
+
$('fileBtn').onclick=function(){
|
| 568 |
+
var f=$('fileIn').files[0];
|
| 569 |
+
if(!f){toast('Select a file first',1);return}
|
| 570 |
+
var btn=$('fileBtn');btn.disabled=true;btn.textContent='Uploading...';
|
| 571 |
+
var fd=new FormData();fd.append('file',f);
|
| 572 |
+
api('/admin/api/accounts/upload',{method:'POST',body:fd})
|
| 573 |
+
.then(function(d){toast('Uploaded '+(d.imported||0)+' accounts'+(d.skipped?' ('+d.skipped+' skipped)':''));$('fileIn').value=''})
|
| 574 |
+
.catch(function(e){toast(e.message,1)})
|
| 575 |
+
.finally(function(){btn.disabled=false;btn.textContent='Upload File'});
|
| 576 |
+
};
|
|
|
|
|
|
|
|
|
|
| 577 |
|
| 578 |
+
/* ========== KEYBOARD ========== */
|
| 579 |
+
document.onkeydown=function(e){
|
| 580 |
+
if(e.key==='Escape'){hide('addModal');hide('pwModal')}
|
| 581 |
+
};
|
| 582 |
+
|
| 583 |
+
/* ========== MAILBOX ========== */
|
| 584 |
+
var mbSTimer=null;
|
| 585 |
+
$('mbSearch').oninput=function(){
|
| 586 |
+
clearTimeout(mbSTimer);
|
| 587 |
+
mbSTimer=setTimeout(filterMbAccts,250);
|
| 588 |
+
};
|
| 589 |
+
|
| 590 |
+
function filterMbAccts(){
|
| 591 |
+
var q=$('mbSearch').value.toLowerCase();
|
| 592 |
+
var items=$('mbAcctList').querySelectorAll('.mb-item');
|
| 593 |
+
items.forEach(function(el){el.style.display=el.getAttribute('data-em').indexOf(q)>=0?'':'none'});
|
| 594 |
+
}
|
| 595 |
|
| 596 |
+
function loadMbAccts(){
|
| 597 |
+
var list=$('mbAcctList');
|
| 598 |
+
list.innerHTML='<div class="mb-empty">Loading...</div>';
|
| 599 |
+
api('/admin/api/accounts?limit=500').then(function(d){
|
| 600 |
+
mbAccts=d.accounts||[];
|
| 601 |
+
if(!mbAccts.length){list.innerHTML='<div class="mb-empty">No accounts</div>';return}
|
| 602 |
+
list.innerHTML=mbAccts.map(function(a){
|
| 603 |
+
return '<div class="mb-item'+(a.id===selAcctId?' on':'')+'" data-id="'+esc(a.id)+'" data-em="'+esc((a.email||'').toLowerCase())+'">'
|
| 604 |
+
+'<div style="font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(a.email||'')+'</div>'
|
| 605 |
+
+'<div style="color:var(--text3);font-size:.75rem;margin-top:.15rem">'+(a.is_active!==false?'Active':'Inactive')+' · '+esc(a.source||'')+'</div>'
|
| 606 |
+
+'</div>';
|
| 607 |
+
}).join('');
|
| 608 |
+
// Click handlers
|
| 609 |
+
list.querySelectorAll('.mb-item').forEach(function(el){
|
| 610 |
+
el.onclick=function(){selectMbAcct(el.getAttribute('data-id'),el.getAttribute('data-em'))};
|
| 611 |
});
|
| 612 |
+
populateCFrom();
|
| 613 |
+
}).catch(function(e){list.innerHTML='<div class="mb-empty">Error loading</div>'});
|
| 614 |
}
|
| 615 |
|
| 616 |
+
function populateCFrom(){
|
| 617 |
+
var s=$('cFrom');
|
| 618 |
+
s.innerHTML=mbAccts.map(function(a){return '<option value="'+esc(a.id)+'" data-em="'+esc(a.email||'')+'">'+esc(a.email||'')+'</option>'}).join('');
|
| 619 |
+
if(selAcctId)s.value=selAcctId;
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
function selectMbAcct(id,email){
|
| 623 |
+
selAcctId=id;selAcctEmail=email||'';
|
| 624 |
+
// highlight
|
| 625 |
+
$('mbAcctList').querySelectorAll('.mb-item').forEach(function(el){
|
| 626 |
+
el.classList.toggle('on',el.getAttribute('data-id')===id);
|
| 627 |
+
});
|
| 628 |
+
$('mbInboxTitle').textContent=selAcctEmail||'Inbox';
|
| 629 |
+
loadMbMsgs();
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
function loadMbMsgs(){
|
| 633 |
+
if(!selAcctId)return;
|
| 634 |
+
var list=$('mbMsgList');
|
| 635 |
+
list.innerHTML='<div class="mb-empty">Loading via IMAP...</div>';
|
| 636 |
+
showMbView('empty');
|
| 637 |
+
api('/admin/api/accounts/'+selAcctId+'/messages').then(function(d){
|
| 638 |
+
mbMsgs=d.messages||[];
|
| 639 |
+
if(!mbMsgs.length){list.innerHTML='<div class="mb-empty">No messages</div>';return}
|
| 640 |
+
list.innerHTML=mbMsgs.map(function(m,i){
|
| 641 |
+
var from=(m.from&&m.from.address)||m.from||'';
|
| 642 |
+
return '<div class="mb-item" data-i="'+i+'">'
|
| 643 |
+
+'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.2rem">'
|
| 644 |
+
+'<span style="font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:.5rem">'+esc(from)+'</span>'
|
| 645 |
+
+(m.verification_code?'<span class="badge b-ok" style="flex-shrink:0;font-size:.68rem">'+esc(m.verification_code)+'</span>':'')
|
| 646 |
+
+'</div>'
|
| 647 |
+
+'<div style="font-weight:500;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(m.subject||'(no subject)')+'</div>'
|
| 648 |
+
+'<div style="color:var(--text3);font-size:.75rem;margin-top:.15rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc((m.intro||'').substring(0,80))+'</div>'
|
| 649 |
+
+'</div>';
|
| 650 |
+
}).join('');
|
| 651 |
+
list.querySelectorAll('.mb-item').forEach(function(el){
|
| 652 |
+
el.onclick=function(){showMbMsg(parseInt(el.getAttribute('data-i')))};
|
| 653 |
+
});
|
| 654 |
+
}).catch(function(e){
|
| 655 |
+
list.innerHTML='<div class="mb-empty" style="color:var(--err)">Error: '+esc(e.message)+'</div>';
|
| 656 |
+
});
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
function showMbView(v){
|
| 660 |
+
hide('mbEmpty');hide('mbDetail');hide('mbCompose');
|
| 661 |
+
if(v==='empty')show('mbEmpty','flex');
|
| 662 |
+
else if(v==='detail'){show('mbDetail');$('mbDetail').style.display='flex';$('mbDetail').style.flexDirection='column'}
|
| 663 |
+
else if(v==='compose'){show('mbCompose');$('mbCompose').style.display='flex';$('mbCompose').style.flexDirection='column'}
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
function showMbMsg(idx){
|
| 667 |
+
var m=mbMsgs[idx];if(!m)return;
|
| 668 |
+
curMsgIdx=idx;
|
| 669 |
+
// highlight
|
| 670 |
+
$('mbMsgList').querySelectorAll('.mb-item').forEach(function(el){
|
| 671 |
+
el.classList.toggle('on',parseInt(el.getAttribute('data-i'))===idx);
|
| 672 |
+
});
|
| 673 |
+
$('mbSubj').textContent=m.subject||'(no subject)';
|
| 674 |
+
$('mbFrom').textContent='From: '+((m.from&&m.from.address)||m.from||'');
|
| 675 |
+
if(m.verification_code){
|
| 676 |
+
$('mbCode').innerHTML='<span class="badge b-ok">Code: '+esc(m.verification_code)+'</span>';
|
| 677 |
+
}else{$('mbCode').innerHTML=''}
|
| 678 |
+
$('mbBody').srcdoc=(m.html&&m.html[0])||m.text||'(empty)';
|
| 679 |
+
showMbView('detail');
|
| 680 |
}
|
| 681 |
|
| 682 |
+
$('mbRefresh').onclick=function(){
|
| 683 |
+
if(selAcctId)loadMbMsgs();else toast('Select an account first',1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
};
|
| 685 |
|
| 686 |
+
// Reply
|
| 687 |
+
$('mbReplyBtn').onclick=function(){
|
| 688 |
+
if(curMsgIdx<0)return;var m=mbMsgs[curMsgIdx];if(!m)return;
|
| 689 |
+
showMbView('compose');
|
| 690 |
+
if(selAcctId)$('cFrom').value=selAcctId;
|
| 691 |
+
$('cTo').value=(m.from&&m.from.address)||m.from||'';
|
| 692 |
+
$('cCc').value='';
|
| 693 |
+
$('cSubj').value=(m.subject||'').indexOf('Re: ')===0?m.subject:'Re: '+(m.subject||'');
|
| 694 |
+
$('cBody').value='\n\n--- Original Message ---\n'+(m.text||'');
|
| 695 |
+
$('cReplyTo').value=m.id||'';$('cRefs').value=m.id||'';
|
| 696 |
+
};
|
| 697 |
+
|
| 698 |
+
// Compose
|
| 699 |
+
$('composeBtn').onclick=function(){
|
| 700 |
+
showMbView('compose');
|
| 701 |
+
if(selAcctId)$('cFrom').value=selAcctId;
|
| 702 |
+
$('cTo').value='';$('cCc').value='';$('cSubj').value='';$('cBody').value='';
|
| 703 |
+
$('cReplyTo').value='';$('cRefs').value='';
|
| 704 |
+
};
|
| 705 |
+
|
| 706 |
+
$('composeCancel').onclick=function(){
|
| 707 |
+
if(curMsgIdx>=0)showMbView('detail');else showMbView('empty');
|
| 708 |
+
};
|
| 709 |
+
|
| 710 |
+
// Send
|
| 711 |
+
$('sendBtn').onclick=function(){
|
| 712 |
+
var fromId=$('cFrom').value,to=$('cTo').value.trim(),subj=$('cSubj').value.trim(),body=$('cBody').value;
|
| 713 |
+
if(!fromId){toast('Select a sender',1);return}
|
| 714 |
+
if(!to){toast('Enter a recipient',1);return}
|
| 715 |
+
if(!subj){toast('Enter a subject',1);return}
|
| 716 |
+
var btn=$('sendBtn');btn.disabled=true;btn.textContent='Sending...';
|
| 717 |
+
api('/admin/api/accounts/'+fromId+'/send',{method:'POST',body:{
|
| 718 |
+
to:to,subject:subj,body_text:body,body_html:'',cc:$('cCc').value.trim(),
|
| 719 |
+
in_reply_to:$('cReplyTo').value,references:$('cRefs').value
|
| 720 |
+
}}).then(function(){toast('Email sent!');showMbView('empty')})
|
| 721 |
+
.catch(function(e){toast(e.message,1)})
|
| 722 |
+
.finally(function(){btn.disabled=false;btn.textContent='Send Email'});
|
| 723 |
+
};
|
| 724 |
|
| 725 |
+
// Open mailbox from accounts tab
|
| 726 |
+
W._mb=function(id,email){
|
| 727 |
+
curTab='mail';
|
| 728 |
+
navBtns.forEach(function(b){b.classList.remove('on');if(b.getAttribute('data-t')==='mail')b.classList.add('on')});
|
| 729 |
+
loadTab('mail');
|
| 730 |
+
setTimeout(function(){selectMbAcct(id,email)},300);
|
|
|
|
|
|
|
| 731 |
};
|
| 732 |
|
| 733 |
+
/* ========== API DOCS ========== */
|
| 734 |
+
var DOCS={
|
| 735 |
+
mail:[
|
| 736 |
+
{m:'GET',p:'/domains',d:'List supported domains',
|
| 737 |
+
curl:'curl https://your-domain/domains',
|
| 738 |
+
py:'import requests\nr = requests.get("https://your-domain/domains")\nprint(r.json())',
|
| 739 |
+
js:'const res = await fetch("/domains");\nconst data = await res.json();'},
|
| 740 |
+
{m:'POST',p:'/accounts',d:'Register account (IMAP validation)',
|
| 741 |
+
curl:'curl -X POST https://your-domain/accounts \\\n -H "Content-Type: application/json" \\\n -d \'{"address":"user@outlook.com","password":"pass"}\'',
|
| 742 |
+
py:'r = requests.post("/accounts",\n json={"address":"user@outlook.com","password":"pass"})',
|
| 743 |
+
js:'const res = await fetch("/accounts", {\n method: "POST",\n headers: {"Content-Type": "application/json"},\n body: JSON.stringify({address:"user@outlook.com",password:"pass"})\n});'},
|
| 744 |
+
{m:'POST',p:'/token',d:'Get JWT token',
|
| 745 |
+
curl:'curl -X POST https://your-domain/token \\\n -H "Content-Type: application/json" \\\n -d \'{"address":"user@outlook.com","password":"pass"}\'',
|
| 746 |
+
py:'r = requests.post("/token",\n json={"address":"user@outlook.com","password":"pass"})\ntoken = r.json()["token"]',
|
| 747 |
+
js:'const res = await fetch("/token", {\n method: "POST",\n headers: {"Content-Type": "application/json"},\n body: JSON.stringify({address:"user@outlook.com",password:"pass"})\n});'},
|
| 748 |
+
{m:'GET',p:'/me',d:'Current user info (Bearer)',
|
| 749 |
+
curl:'curl /me -H "Authorization: Bearer TOKEN"',
|
| 750 |
+
py:'r = requests.get("/me", headers={"Authorization":"Bearer TOKEN"})',
|
| 751 |
+
js:'const res = await fetch("/me", {headers:{"Authorization":"Bearer TOKEN"}});'},
|
| 752 |
+
{m:'GET',p:'/messages',d:'List inbox messages (Bearer)',
|
| 753 |
+
curl:'curl "/messages?page=1" -H "Authorization: Bearer TOKEN"',
|
| 754 |
+
py:'r = requests.get("/messages", params={"page":1},\n headers={"Authorization":"Bearer TOKEN"})',
|
| 755 |
+
js:'const res = await fetch("/messages?page=1", {headers:{"Authorization":"Bearer TOKEN"}});'},
|
| 756 |
+
{m:'GET',p:'/messages/{id}',d:'Get specific message (Bearer)',
|
| 757 |
+
curl:'curl /messages/123 -H "Authorization: Bearer TOKEN"',
|
| 758 |
+
py:'r = requests.get("/messages/123", headers={"Authorization":"Bearer TOKEN"})',
|
| 759 |
+
js:'const res = await fetch("/messages/123", {headers:{"Authorization":"Bearer TOKEN"}});'},
|
| 760 |
+
{m:'GET',p:'/messages/{id}/code',d:'Extract verification code',
|
| 761 |
+
curl:'curl /messages/123/code -H "Authorization: Bearer TOKEN"',
|
| 762 |
+
py:'r = requests.get("/messages/123/code",\n headers={"Authorization":"Bearer TOKEN"})\ncode = r.json()["verification_code"]',
|
| 763 |
+
js:'const {verification_code} = await (await fetch("/messages/123/code",\n {headers:{"Authorization":"Bearer TOKEN"}})).json();'},
|
| 764 |
+
{m:'DELETE',p:'/accounts/me',d:'Delete account (Bearer)',
|
| 765 |
+
curl:'curl -X DELETE /accounts/me -H "Authorization: Bearer TOKEN"',
|
| 766 |
+
py:'r = requests.delete("/accounts/me", headers={"Authorization":"Bearer TOKEN"})',
|
| 767 |
+
js:'await fetch("/accounts/me", {method:"DELETE", headers:{"Authorization":"Bearer TOKEN"}});'}
|
| 768 |
+
],
|
| 769 |
+
admin:[
|
| 770 |
+
{m:'POST',p:'/admin/api/login',d:'Authenticate',
|
| 771 |
+
curl:'curl -X POST /admin/api/login \\\n -H "Content-Type: application/json" -d \'{"password":"pw"}\'',
|
| 772 |
+
py:'r = requests.post("/admin/api/login", json={"password":"pw"})\ntoken = r.json()["token"]',
|
| 773 |
+
js:'const {token} = await (await fetch("/admin/api/login",{method:"POST",\n headers:{"Content-Type":"application/json"},body:JSON.stringify({password:"pw"})})).json();'},
|
| 774 |
+
{m:'GET',p:'/admin/api/stats',d:'Account statistics',
|
| 775 |
+
curl:'curl /admin/api/stats -H "Authorization: Bearer TOKEN"',py:'r = requests.get("/admin/api/stats", headers={"Authorization":"Bearer TOKEN"})',
|
| 776 |
+
js:'const stats = await (await fetch("/admin/api/stats", {headers:{"Authorization":"Bearer TOKEN"}})).json();'},
|
| 777 |
+
{m:'GET',p:'/admin/api/accounts',d:'List accounts (?page&search&active)',
|
| 778 |
+
curl:'curl "/admin/api/accounts?page=1" -H "Authorization: Bearer TOKEN"',
|
| 779 |
+
py:'r = requests.get("/admin/api/accounts", params={"page":1},\n headers={"Authorization":"Bearer TOKEN"})',
|
| 780 |
+
js:'const data = await (await fetch("/admin/api/accounts?page=1",\n {headers:{"Authorization":"Bearer TOKEN"}})).json();'},
|
| 781 |
+
{m:'POST',p:'/admin/api/accounts',d:'Add single account',
|
| 782 |
+
curl:'curl -X POST /admin/api/accounts \\\n -H "Authorization: Bearer TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"email":"user@outlook.com","password":"pass"}\'',
|
| 783 |
+
py:'r = requests.post("/admin/api/accounts",\n json={"email":"user@outlook.com","password":"pass"},\n headers={"Authorization":"Bearer TOKEN"})',
|
| 784 |
+
js:'await fetch("/admin/api/accounts",{method:"POST",\n headers:{"Content-Type":"application/json","Authorization":"Bearer TOKEN"},\n body:JSON.stringify({email:"user@outlook.com",password:"pass"})});'},
|
| 785 |
+
{m:'POST',p:'/admin/api/accounts/bulk',d:'Bulk import',
|
| 786 |
+
curl:'curl -X POST /admin/api/accounts/bulk \\\n -H "Authorization: Bearer TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"accounts":["user@outlook.com:pass"]}\'',
|
| 787 |
+
py:'r = requests.post("/admin/api/accounts/bulk",\n json={"accounts":["user@outlook.com:pass"]},\n headers={"Authorization":"Bearer TOKEN"})',
|
| 788 |
+
js:'await fetch("/admin/api/accounts/bulk",{method:"POST",\n headers:{"Content-Type":"application/json","Authorization":"Bearer TOKEN"},\n body:JSON.stringify({accounts:["user@outlook.com:pass"]})});'},
|
| 789 |
+
{m:'PATCH',p:'/admin/api/accounts/{id}',d:'Toggle active/update',
|
| 790 |
+
curl:'curl -X PATCH /admin/api/accounts/123 \\\n -H "Authorization: Bearer TOKEN" \\\n -H "Content-Type: application/json" -d \'{"is_active":true}\'',
|
| 791 |
+
py:'r = requests.patch("/admin/api/accounts/123",\n json={"is_active":True}, headers={"Authorization":"Bearer TOKEN"})',
|
| 792 |
+
js:'await fetch("/admin/api/accounts/123",{method:"PATCH",\n headers:{"Content-Type":"application/json","Authorization":"Bearer TOKEN"},\n body:JSON.stringify({is_active:true})});'},
|
| 793 |
+
{m:'DELETE',p:'/admin/api/accounts/{id}',d:'Delete account',
|
| 794 |
+
curl:'curl -X DELETE /admin/api/accounts/123 -H "Authorization: Bearer TOKEN"',
|
| 795 |
+
py:'r = requests.delete("/admin/api/accounts/123", headers={"Authorization":"Bearer TOKEN"})',
|
| 796 |
+
js:'await fetch("/admin/api/accounts/123",{method:"DELETE",headers:{"Authorization":"Bearer TOKEN"}});'},
|
| 797 |
+
{m:'GET',p:'/admin/api/accounts/{id}/password',d:'Reveal password',
|
| 798 |
+
curl:'curl /admin/api/accounts/123/password -H "Authorization: Bearer TOKEN"',
|
| 799 |
+
py:'r = requests.get("/admin/api/accounts/123/password",\n headers={"Authorization":"Bearer TOKEN"})\npw = r.json()["password"]',
|
| 800 |
+
js:'const {password} = await (await fetch("/admin/api/accounts/123/password",\n {headers:{"Authorization":"Bearer TOKEN"}})).json();'},
|
| 801 |
+
{m:'GET',p:'/admin/api/accounts/{id}/messages',d:'Fetch mailbox via IMAP',
|
| 802 |
+
curl:'curl /admin/api/accounts/123/messages -H "Authorization: Bearer TOKEN"',
|
| 803 |
+
py:'r = requests.get("/admin/api/accounts/123/messages",\n headers={"Authorization":"Bearer TOKEN"})',
|
| 804 |
+
js:'const {messages} = await (await fetch("/admin/api/accounts/123/messages",\n {headers:{"Authorization":"Bearer TOKEN"}})).json();'},
|
| 805 |
+
{m:'POST',p:'/admin/api/accounts/{id}/send',d:'Send email via SMTP',
|
| 806 |
+
curl:'curl -X POST /admin/api/accounts/123/send \\\n -H "Authorization: Bearer TOKEN" \\\n -H "Content-Type: application/json" \\\n -d \'{"to":"r@example.com","subject":"Hi","body_text":"Hello!"}\'',
|
| 807 |
+
py:'r = requests.post("/admin/api/accounts/123/send",\n json={"to":"r@example.com","subject":"Hi","body_text":"Hello!"},\n headers={"Authorization":"Bearer TOKEN"})',
|
| 808 |
+
js:'await fetch("/admin/api/accounts/123/send",{method:"POST",\n headers:{"Content-Type":"application/json","Authorization":"Bearer TOKEN"},\n body:JSON.stringify({to:"r@example.com",subject:"Hi",body_text:"Hello!"})});'},
|
| 809 |
+
{m:'GET',p:'/admin/api/export',d:'Export accounts as .txt',
|
| 810 |
+
curl:'curl /admin/api/export -H "Authorization: Bearer TOKEN" -o accounts.txt',
|
| 811 |
+
py:'r = requests.get("/admin/api/export", headers={"Authorization":"Bearer TOKEN"})\nwith open("accounts.txt","w") as f: f.write(r.text)',
|
| 812 |
+
js:'const text = await (await fetch("/admin/api/export",{headers:{"Authorization":"Bearer TOKEN"}})).text();'}
|
| 813 |
+
]};
|
| 814 |
+
|
| 815 |
+
var MC={'GET':'m-get','POST':'m-post','PATCH':'m-patch','DELETE':'m-del'};
|
| 816 |
+
var docsOk=false;
|
| 817 |
+
|
| 818 |
+
function renderDocs(){
|
| 819 |
+
if(docsOk)return;docsOk=true;
|
| 820 |
+
renderDocGrp('docsMailBox',DOCS.mail,'dM');
|
| 821 |
+
renderDocGrp('docsAdminBox',DOCS.admin,'dA');
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
function renderDocGrp(cid,eps,pfx){
|
| 825 |
+
var c=$(cid);
|
| 826 |
+
eps.forEach(function(ep,i){
|
| 827 |
+
var id=pfx+i;
|
| 828 |
+
var card=document.createElement('div');card.className='doc-card';
|
| 829 |
+
card.innerHTML='<div class="doc-head" onclick="W._docTog(\''+id+'\')">'
|
| 830 |
+
+'<span class="meth '+(MC[ep.m]||'')+'">'+ep.m+'</span>'
|
| 831 |
+
+'<span style="font-family:monospace;font-size:.86rem">'+esc(ep.p)+'</span>'
|
| 832 |
+
+'<span style="margin-left:auto;color:var(--text3);font-size:.78rem">'+esc(ep.d)+'</span>'
|
| 833 |
+
+'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0;transition:transform .2s" id="'+id+'A"><polyline points="6 9 12 15 18 9"/></svg>'
|
| 834 |
+
+'</div>'
|
| 835 |
+
+'<div class="doc-body" id="'+id+'B">'
|
| 836 |
+
+'<div class="doc-tabs">'
|
| 837 |
+
+'<span class="doc-tab on" onclick="W._docLang(\''+id+'\',\'curl\',this)">curl</span>'
|
| 838 |
+
+'<span class="doc-tab" onclick="W._docLang(\''+id+'\',\'py\',this)">Python</span>'
|
| 839 |
+
+'<span class="doc-tab" onclick="W._docLang(\''+id+'\',\'js\',this)">JavaScript</span>'
|
| 840 |
+
+'<span style="margin-left:auto;font-size:.76rem;color:var(--text3);cursor:pointer" onclick="W._docCp(\''+id+'\')">Copy</span>'
|
| 841 |
+
+'</div>'
|
| 842 |
+
+'<pre class="doc-pre" id="'+id+'C">'+esc(ep.curl)+'</pre>'
|
| 843 |
+
+'<span style="display:none" id="'+id+'_curl">'+esc(ep.curl)+'</span>'
|
| 844 |
+
+'<span style="display:none" id="'+id+'_py">'+esc(ep.py)+'</span>'
|
| 845 |
+
+'<span style="display:none" id="'+id+'_js">'+esc(ep.js)+'</span>'
|
| 846 |
+
+'</div>';
|
| 847 |
+
c.appendChild(card);
|
| 848 |
+
});
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
W._docTog=function(id){
|
| 852 |
+
var b=$(id+'B'),a=$(id+'A');
|
| 853 |
+
if(b.style.display==='block'){b.style.display='none';a.style.transform=''}
|
| 854 |
+
else{b.style.display='block';a.style.transform='rotate(180deg)'}
|
| 855 |
+
};
|
| 856 |
+
W._docLang=function(id,lang,el){
|
| 857 |
+
var src=$(id+'_'+lang),code=$(id+'C');
|
| 858 |
+
if(src&&code)code.textContent=src.textContent;
|
| 859 |
+
el.parentNode.querySelectorAll('.doc-tab').forEach(function(t){t.classList.remove('on')});
|
| 860 |
+
el.classList.add('on');
|
| 861 |
+
};
|
| 862 |
+
W._docCp=function(id){
|
| 863 |
+
var code=$(id+'C');
|
| 864 |
+
navigator.clipboard.writeText(code.textContent).then(function(){toast('Copied!')});
|
| 865 |
+
};
|
| 866 |
+
|
| 867 |
+
/* ========== INIT ========== */
|
| 868 |
checkAuth();
|
| 869 |
+
|
| 870 |
})();
|
| 871 |
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 872 |
</body>
|
| 873 |
</html>
|
outlook2api/static/index.html
CHANGED
|
@@ -4,61 +4,169 @@
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<title>Outlook2API</title>
|
|
|
|
|
|
|
| 7 |
<style>
|
| 8 |
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
a{
|
| 12 |
-
|
| 13 |
-
.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
.
|
| 17 |
-
.
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
.
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
.
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
.
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
</style>
|
| 30 |
</head>
|
| 31 |
<body>
|
| 32 |
-
<div class="
|
| 33 |
<div class="hero">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
<h1>Outlook2API</h1>
|
| 35 |
-
<p>
|
| 36 |
</div>
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
<div class="links">
|
| 42 |
-
<a href="/admin" class="link
|
| 43 |
-
<div><
|
| 44 |
-
<
|
| 45 |
-
|
| 46 |
-
<a href="/docs" class="link-card">
|
| 47 |
-
<div><div class="label">API Documentation</div><div class="desc">Interactive Swagger UI</div></div>
|
| 48 |
-
<span class="arrow">→</span>
|
| 49 |
</a>
|
| 50 |
-
<a href="/
|
| 51 |
-
<div><
|
| 52 |
-
<
|
|
|
|
| 53 |
</a>
|
| 54 |
</div>
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</div>
|
| 57 |
<script>
|
| 58 |
-
fetch('/admin/api/stats').then(r
|
| 59 |
-
document.getElementById('
|
| 60 |
-
document.getElementById('
|
| 61 |
-
}).catch(()
|
|
|
|
|
|
|
|
|
|
| 62 |
</script>
|
| 63 |
</body>
|
| 64 |
</html>
|
|
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<title>Outlook2API</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
<style>
|
| 10 |
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 11 |
+
body{font-family:'DM Sans',system-ui,-apple-system,sans-serif;min-height:100vh;color:#1a1a1a;line-height:1.6;
|
| 12 |
+
background:linear-gradient(135deg,#fef7f0 0%,#f8f7f4 40%,#f0f4f8 100%)}
|
| 13 |
+
a{text-decoration:none;transition:color .15s}
|
| 14 |
+
|
| 15 |
+
.page{max-width:820px;margin:0 auto;padding:64px 24px 48px}
|
| 16 |
+
|
| 17 |
+
/* Hero */
|
| 18 |
+
.hero{text-align:center;margin-bottom:52px}
|
| 19 |
+
.pill{display:inline-flex;align-items:center;gap:6px;padding:6px 16px;
|
| 20 |
+
background:linear-gradient(135deg,rgba(201,100,66,.1),rgba(201,100,66,.05));
|
| 21 |
+
border:1px solid rgba(201,100,66,.15);border-radius:99px;font-size:12px;font-weight:600;
|
| 22 |
+
color:#c96442;margin-bottom:24px;letter-spacing:.02em}
|
| 23 |
+
.pill svg{width:14px;height:14px}
|
| 24 |
+
h1{font-size:36px;font-weight:700;letter-spacing:-.03em;margin-bottom:10px;
|
| 25 |
+
background:linear-gradient(135deg,#c96442,#8b5cf6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
|
| 26 |
+
.hero p{color:#6b6560;font-size:16px;max-width:500px;margin:0 auto}
|
| 27 |
+
|
| 28 |
+
/* Stats */
|
| 29 |
+
.stats{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:40px}
|
| 30 |
+
.stat{background:#fff;border:1px solid #e8e5e0;border-radius:12px;padding:22px 26px;position:relative;overflow:hidden;transition:transform .2s,box-shadow .2s}
|
| 31 |
+
.stat:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.06)}
|
| 32 |
+
.stat::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
|
| 33 |
+
.stat:first-child::before{background:linear-gradient(90deg,#c96442,#e8956d)}
|
| 34 |
+
.stat:last-child::before{background:linear-gradient(90deg,#2d8a4e,#5cb85c)}
|
| 35 |
+
.stat-l{font-size:11px;font-weight:600;color:#9b958e;text-transform:uppercase;letter-spacing:.08em}
|
| 36 |
+
.stat-v{font-size:32px;font-weight:700;margin-top:6px;letter-spacing:-.02em}
|
| 37 |
+
.stat:last-child .stat-v{color:#2d8a4e}
|
| 38 |
+
|
| 39 |
+
/* Features */
|
| 40 |
+
.features{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:40px}
|
| 41 |
+
.feat{background:#fff;border:1px solid #e8e5e0;border-radius:12px;padding:22px;transition:transform .2s,box-shadow .2s}
|
| 42 |
+
.feat:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.06)}
|
| 43 |
+
.feat-ico{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;margin-bottom:14px}
|
| 44 |
+
.feat-ico svg{width:20px;height:20px}
|
| 45 |
+
.fi-mail{background:linear-gradient(135deg,#dbeafe,#eff6ff);color:#2563eb}
|
| 46 |
+
.fi-admin{background:linear-gradient(135deg,#fce7d6,#fef3ec);color:#c96442}
|
| 47 |
+
.fi-code{background:linear-gradient(135deg,#d1fae5,#ecfdf5);color:#059669}
|
| 48 |
+
.fi-ci{background:linear-gradient(135deg,#ede9fe,#f5f3ff);color:#7c3aed}
|
| 49 |
+
.feat h3{font-size:14px;font-weight:600;margin-bottom:4px}
|
| 50 |
+
.feat p{font-size:13px;color:#6b6560;line-height:1.5}
|
| 51 |
+
.feat code{background:#f3f4f6;padding:1px 5px;border-radius:3px;font-size:12px}
|
| 52 |
+
|
| 53 |
+
/* Links */
|
| 54 |
+
.links{display:flex;flex-direction:column;gap:12px;margin-bottom:40px}
|
| 55 |
+
.link{display:flex;align-items:center;gap:16px;background:#fff;border:1px solid #e8e5e0;border-radius:12px;padding:18px 22px;transition:all .2s;color:inherit}
|
| 56 |
+
.link:hover{box-shadow:0 8px 24px rgba(0,0,0,.06);border-color:#c96442;transform:translateY(-1px)}
|
| 57 |
+
.link-ico{width:44px;height:44px;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
| 58 |
+
.link-ico svg{width:22px;height:22px}
|
| 59 |
+
.li-admin{background:linear-gradient(135deg,#fce7d6,#fef3ec);color:#c96442}
|
| 60 |
+
.li-docs{background:linear-gradient(135deg,#dbeafe,#eff6ff);color:#2563eb}
|
| 61 |
+
.link-txt{flex:1}
|
| 62 |
+
.link-t{font-size:15px;font-weight:600}
|
| 63 |
+
.link-d{font-size:13px;color:#6b6560;margin-top:1px}
|
| 64 |
+
.link-arr{color:#9b958e;font-size:20px;transition:transform .2s}
|
| 65 |
+
.link:hover .link-arr{transform:translateX(4px);color:#c96442}
|
| 66 |
+
|
| 67 |
+
/* API Quick Ref */
|
| 68 |
+
.api-ref{background:#fff;border:1px solid #e8e5e0;border-radius:12px;overflow:hidden;margin-bottom:40px}
|
| 69 |
+
.api-hdr{padding:16px 22px;border-bottom:1px solid #e8e5e0;display:flex;align-items:center;justify-content:space-between}
|
| 70 |
+
.api-hdr h3{font-size:14px;font-weight:600}.api-hdr span{font-size:12px;color:#9b958e}
|
| 71 |
+
.ep{display:flex;align-items:center;gap:14px;padding:10px 22px;border-bottom:1px solid #f3f0ec;font-size:13px}
|
| 72 |
+
.ep:last-child{border-bottom:none}
|
| 73 |
+
.ep:hover{background:#faf9f7}
|
| 74 |
+
.m{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;font-family:monospace;min-width:48px;text-align:center}
|
| 75 |
+
.m-g{background:#dcfce7;color:#16a34a}.m-p{background:#dbeafe;color:#2563eb}.m-d{background:#fee2e2;color:#dc2626}
|
| 76 |
+
.ep-p{font-family:monospace;color:#1a1a1a}
|
| 77 |
+
.ep-d{color:#9b958e;margin-left:auto;font-size:12px}
|
| 78 |
+
|
| 79 |
+
/* Footer */
|
| 80 |
+
.foot{text-align:center;padding-top:16px;border-top:1px solid #e8e5e0;color:#9b958e;font-size:13px}
|
| 81 |
+
.foot a{color:#6b6560;font-weight:500}
|
| 82 |
+
.foot a:hover{color:#c96442}
|
| 83 |
+
|
| 84 |
+
@media(max-width:560px){
|
| 85 |
+
.page{padding:40px 16px 32px}
|
| 86 |
+
h1{font-size:28px}
|
| 87 |
+
.features{grid-template-columns:1fr}
|
| 88 |
+
.ep-d{display:none}
|
| 89 |
+
}
|
| 90 |
</style>
|
| 91 |
</head>
|
| 92 |
<body>
|
| 93 |
+
<div class="page">
|
| 94 |
<div class="hero">
|
| 95 |
+
<div class="pill">
|
| 96 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
| 97 |
+
Mail.tm-compatible API
|
| 98 |
+
</div>
|
| 99 |
<h1>Outlook2API</h1>
|
| 100 |
+
<p>REST API for Outlook/Hotmail/Live accounts with admin panel, webmail, and batch registration</p>
|
| 101 |
</div>
|
| 102 |
+
|
| 103 |
+
<div class="stats">
|
| 104 |
+
<div class="stat">
|
| 105 |
+
<div class="stat-l">Total Accounts</div>
|
| 106 |
+
<div class="stat-v" id="st">—</div>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="stat">
|
| 109 |
+
<div class="stat-l">Active</div>
|
| 110 |
+
<div class="stat-v" id="sa">—</div>
|
| 111 |
+
</div>
|
| 112 |
</div>
|
| 113 |
+
|
| 114 |
+
<div class="features">
|
| 115 |
+
<div class="feat">
|
| 116 |
+
<div class="feat-ico fi-mail"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg></div>
|
| 117 |
+
<h3>Mail API</h3>
|
| 118 |
+
<p>Mail.tm-compatible Hydra API for domains, accounts, tokens, and messages</p>
|
| 119 |
+
</div>
|
| 120 |
+
<div class="feat">
|
| 121 |
+
<div class="feat-ico fi-admin"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg></div>
|
| 122 |
+
<h3>Admin Panel</h3>
|
| 123 |
+
<p>Web UI for account management, webmail with compose & reply, bulk import/export</p>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="feat">
|
| 126 |
+
<div class="feat-ico fi-code"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></div>
|
| 127 |
+
<h3>Verification Codes</h3>
|
| 128 |
+
<p>Auto-extract 6-digit OTP from emails via <code>/messages/{id}/code</code></p>
|
| 129 |
+
</div>
|
| 130 |
+
<div class="feat">
|
| 131 |
+
<div class="feat-ico fi-ci"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div>
|
| 132 |
+
<h3>CI Auto-Import</h3>
|
| 133 |
+
<p>GitHub Actions for automated Outlook registration & account import</p>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
<div class="links">
|
| 138 |
+
<a href="/admin" class="link">
|
| 139 |
+
<div class="link-ico li-admin"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
|
| 140 |
+
<div class="link-txt"><div class="link-t">Admin Panel</div><div class="link-d">Manage accounts, webmail, import/export</div></div>
|
| 141 |
+
<span class="link-arr">→</span>
|
|
|
|
|
|
|
|
|
|
| 142 |
</a>
|
| 143 |
+
<a href="/docs" class="link">
|
| 144 |
+
<div class="link-ico li-docs"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></div>
|
| 145 |
+
<div class="link-txt"><div class="link-t">API Documentation</div><div class="link-d">Interactive Swagger UI with all endpoints</div></div>
|
| 146 |
+
<span class="link-arr">→</span>
|
| 147 |
</a>
|
| 148 |
</div>
|
| 149 |
+
|
| 150 |
+
<div class="api-ref">
|
| 151 |
+
<div class="api-hdr"><h3>Quick API Reference</h3><span>Mail.tm-compatible</span></div>
|
| 152 |
+
<div class="ep"><span class="m m-g">GET</span><span class="ep-p">/domains</span><span class="ep-d">List email domains</span></div>
|
| 153 |
+
<div class="ep"><span class="m m-p">POST</span><span class="ep-p">/accounts</span><span class="ep-d">Register account</span></div>
|
| 154 |
+
<div class="ep"><span class="m m-p">POST</span><span class="ep-p">/token</span><span class="ep-d">Get JWT token</span></div>
|
| 155 |
+
<div class="ep"><span class="m m-g">GET</span><span class="ep-p">/messages</span><span class="ep-d">List inbox messages</span></div>
|
| 156 |
+
<div class="ep"><span class="m m-g">GET</span><span class="ep-p">/messages/{id}/code</span><span class="ep-d">Extract verification code</span></div>
|
| 157 |
+
<div class="ep"><span class="m m-d">DELETE</span><span class="ep-p">/accounts/me</span><span class="ep-d">Delete account</span></div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div class="foot">Outlook2API · <a href="https://github.com/shenhao-stu/outlook2api">GitHub</a></div>
|
| 161 |
</div>
|
| 162 |
<script>
|
| 163 |
+
fetch('/admin/api/public-stats').then(function(r){if(r.ok)return r.json();throw r}).then(function(d){
|
| 164 |
+
document.getElementById('st').textContent=d.total!=null?d.total:0;
|
| 165 |
+
document.getElementById('sa').textContent=d.active!=null?d.active:0;
|
| 166 |
+
}).catch(function(){
|
| 167 |
+
document.getElementById('st').textContent='0';
|
| 168 |
+
document.getElementById('sa').textContent='0';
|
| 169 |
+
});
|
| 170 |
</script>
|
| 171 |
</body>
|
| 172 |
</html>
|