88 lines
7.3 KiB
Markdown
88 lines
7.3 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Что это
|
||
|
||
`mcimport` — однобинарный инструмент (Go, только stdlib) для массового создания
|
||
почтовых ящиков в mailcow из CSV через его REST API. Веб-морда встроена в бинарь
|
||
(`//go:embed index.html`). Пайплайн пользователя: загрузка CSV → предпросмотр с
|
||
валидацией → потоковый импорт с журналом.
|
||
|
||
## Команды
|
||
|
||
```bash
|
||
# Сборка статического бинаря
|
||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o mcimport .
|
||
|
||
# Локальный запуск (UI на http://localhost:8080)
|
||
MAILCOW_URL=https://mail.dom.ru ./mcimport
|
||
|
||
# Docker (порт публикуется только на 127.0.0.1 — ходить через SSH-туннель)
|
||
docker compose up -d --build
|
||
|
||
go vet ./...
|
||
gofmt -l . # список неотформатированных файлов; -w для правки
|
||
```
|
||
|
||
Тестов в проекте нет. `go.mod` не имеет внешних зависимостей — только stdlib,
|
||
поэтому `go mod tidy`/`go get` при обычной работе не требуются.
|
||
|
||
## Архитектура
|
||
|
||
Весь backend — один файл `main.go`, весь frontend — один `index.html` (встроен).
|
||
Состояние на сервере не хранится: инструмент **stateless**, API-ключ и пароли
|
||
не пишутся на диск и не логируются.
|
||
|
||
Четыре HTTP-эндпоинта (`net/http` ServeMux с method-роутингом Go 1.22):
|
||
|
||
- `GET /api/config` — отдаёт дефолты в UI (base URL, есть ли ключ в env, дефолт-квота).
|
||
- `GET /api/example` — отдаёт образец CSV (`exampleCSV`) как вложение для скачивания.
|
||
- `POST /api/test` — проверка связи: читает домены и ящики, подтверждает ключ и allow-list.
|
||
- `POST /api/preview` — разбор CSV (multipart), валидация, обогащение против живого mailcow. Form-поле `domain` включает режим override (см. ниже). В ответе — `quotaWarnings` (проверка доменной квоты).
|
||
- `POST /api/import` — создание ящиков, **потоковый NDJSON** (по строке результата на ящик, с `flusher.Flush()`).
|
||
|
||
Три конвейерных этапа обработки строки CSV, каждый меняет `Row.Status`
|
||
(`create`→`warn`→`exists`→`error`) и флаг `Importable`:
|
||
|
||
1. `parseCSV` → `validateRow` — структурная проверка без сети (email, пароль, дубликаты в файле).
|
||
2. `enrichRow` — сверка с живым mailcow: несуществующий домен → `error`, существующий ящик → `exists` (пропуск). Работает только если заданы креды.
|
||
3. `checkDomainQuota` — воспроизводит проверку mailcow: свободная квота домена = `max_quota_for_domain` − Σ(выделенных квот существующих ящиков). Если Σ квот импортируемых строк домена превышает свободную — возвращает `quotaWarn` (в МиБ). UI показывает предупреждение и **блокирует** импорт до подъёма квоты домена (домены с квотой 0 = безлимит пропускаются).
|
||
4. `handleImport` — POST на `/api/v1/add/mailbox`, ответ разбирается `parseMailcowMsg`. Используется **только** `add` — существующие ящики никогда не модифицируются (идемпотентность).
|
||
|
||
### Ключевые соглашения
|
||
|
||
- **Разрешение кредов** — `resolveCreds`: значения из тела запроса приоритетнее env
|
||
(`MAILCOW_URL`/`MAILCOW_API_KEY`). Ключ можно задать через env и не вводить в UI.
|
||
- **Ответы API mailcow** имеют форму `[{"type": "...", "msg": ...}]`; `type != "success"`
|
||
считается провалом. `msg` бывает строкой или массивом — разворачивается в `flattenMsg`.
|
||
- **CSV-парсинг** терпимый: авто-определение разделителя (`sniffDelimiter`: `,`/`;`/таб;
|
||
на практике используется `;`), снятие BOM, `LazyQuotes`, RU-алиасы заголовков
|
||
(`headerAliases` + `normHeader` игнорирует регистр/пробелы/подчёркивания).
|
||
`email` можно заменить парой `localpart`+`domain`.
|
||
- **Режим override-домена** (`parseCSV(..., overrideDomain)`): если домен выбран в UI
|
||
и передан form-полем `domain`, колонка логина (`email`/`login`/`localpart`)
|
||
трактуется как localpart без домена, email собирается как `localpart@overrideDomain`.
|
||
Полный email в значении терпится — берётся часть до `@` с пометкой в `Notes`.
|
||
Домен нормализуется в lowercase в `handlePreview`.
|
||
- Ошибки пользователю — **на русском**; комментарии в коде — на английском. Держать этот сплит.
|
||
- `INSECURE_SKIP_VERIFY=true` отключает проверку TLS (для внутреннего CA mailcow).
|
||
|
||
- Дефолт квоты — константа `defaultQuotaMiB` (10240 МиБ). Одна точка правды;
|
||
дублируется в `docker-compose.yml`, `README.md` и `value` поля `#quota` в `index.html`.
|
||
- **Единицы квот**: `fetchDomains`/`fetchMailboxes` возвращают квоты в **байтах**
|
||
(как отдаёт mailcow), `Row.Quota` — в **МиБ** (строка). `checkDomainQuota` сводит
|
||
всё к байтам, `quotaWarn` наружу — в МиБ. UI форматирует МиБ→ГБ (`gb()`).
|
||
- **Повтор в UI без перезагрузки файла** (`index.html`): `previewRows`/`importResults`
|
||
держатся в памяти. «Повторить проверку» = повторный `runPreview()` (файл в `fileInput`
|
||
сохраняется). «Повторить неудавшиеся» = `runImport()` по строкам с `ok:false` из
|
||
`importResults` — идемпотентно, т.к. упавшие ящики не создались, успешные не трогаются.
|
||
|
||
При добавлении полей ящика править **три места синхронно**: заголовки/алиасы CSV
|
||
(`headerAliases`), структуру `Row` + `validateRow`, и `payload` в `handleImport`.
|
||
|
||
## Требования на стороне mailcow
|
||
|
||
- API-ключ с правами **Read-Write** (`Configuration → Access → API`).
|
||
- IP, с которого ходит mcimport, добавлен в `API_ALLOW_FROM` — иначе отказ доступа (401/403).
|