feat: wire email-api into docker-compose, .sln and CLAUDE.md

- docker-compose: add email-api service (internal, no ports)
  with Smtp__* + FileStorage__Path + Files volume mount
- api + cv-search-job: remove Smtp__* vars, add EmailApi__BaseUrl
  and EmailApi__InternalApiKey; add depends_on: email-api
- .sln: move email-api-models to Models virtual folder
- CLAUDE.md: add email-api/email-api-models to layout, update
  service dependency diagram and internal API key table

Closes #22

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 16:26:56 +03:00
parent 8878efe184
commit ba92c9f793
3 changed files with 64 additions and 21 deletions
+22 -10
View File
@@ -35,7 +35,7 @@ This applies to both the staging and production repos as appropriate.
- Entity Framework Core + SQL Server (multi-schema)
- Refit for typed HTTP clients between services
- Serilog (JSON structured logging, Console + File + Email sinks)
- MailKit for SMTP
- MailKit for SMTP (used exclusively in `email-api`)
- Docker Compose for local and production deployment
- Watchtower for automatic container updates in production
@@ -63,6 +63,8 @@ This applies to both the staging and production repos as appropriate.
Apis/
api/ Public-facing proxy API (port 8080). Handles CORS, rate limiting, captcha, email.
api-models/ DTOs and settings for api only.
email-api/ Internal SMTP email relay (no public port). All email sending goes here.
email-api-models/ Refit client + SendEmailRequest + EmailApiSettings (shared by api and cv-search-job).
cv-matcher-api/ Internal CV match engine (port 8082). Runs CvMatcher + CvSearch + MyAi DB migrations.
cv-matcher-api-models/ DTOs shared between api and cv-matcher-api (incl. JobSearchSettings).
rag-api/ Internal RAG/vector-search service (port 8081).
@@ -148,26 +150,36 @@ EF tools version warning ("older than runtime") is expected and harmless. The `H
```
web → api → cv-matcher-api → rag-api
cv-search-job
↓ ↓
| email-api
↓ ↑
cv-search-job
```
`api` and `cv-search-job` both call `email-api` for all outbound email (SMTP).
`api` never talks directly to `rag-api` — always via `cv-matcher-api`.
## Internal API key auth
All internal service-to-service calls require the `X-Internal-Api-Key` header.
The key is shared via the `CvMatcherApi__InternalApiKey` and `RagApi__InternalApiKey` env vars.
`startup-helpers` provides `UseInternalApiKeyProtection()` middleware that enforces it on `cv-matcher-api` and `rag-api`.
All internal service-to-service calls require the `X-Internal-Api-Key` header.
| Caller | Target | Env var for key |
|--------|--------|-----------------|
| `api`, `cv-search-job` | `email-api` | `EmailApi__InternalApiKey` |
| `api`, `cv-search-job` | `cv-matcher-api` | `CvMatcherApi__InternalApiKey` |
| `cv-matcher-api` | `rag-api` | `RagApi__InternalApiKey` |
`startup-helpers` provides `UseInternalApiKeyProtection()` middleware (reads `InternalApi:ApiKey`); enforced on `cv-matcher-api`, `rag-api`, and `email-api`.
## Shared file storage
CV PDFs are written by `api` to `Apis/api/Files/` and read by `cv-cleanup-job` and `cv-search-job`.
All three containers mount the same bind volume:
CV PDFs are written by `api` to `Apis/api/Files/` and read by `cv-cleanup-job`, `cv-search-job`, and `email-api` (for email attachments).
All four containers mount the same bind volume:
```yaml
- ../Apis/api/Files:/app/Files
- ${FILES_PATH:-/opt/myai/files}:/app/Files
```
The path inside containers is controlled by `FileStorage__Path` (default: `Files`).
The path inside containers is controlled by `FileStorage__Path` (default: `Files`).
`email-api` receives only the relative filename (e.g. `abc123.pdf`) and resolves it against `FileStorage__Path`.
## Job task pattern