internal/cli/http.go (view raw)
1package cli
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "log/slog"
9 "net"
10 "net/http"
11 "os"
12 "path/filepath"
13 "time"
14
15 "postern/internal/model"
16)
17
18// HTTPService implements the cli.Service interface for serving HTTP endpoints.
19type HTTPService struct {
20 cmd *Command
21 socketPath string
22 srv *http.Server
23}
24
25// NewHTTPService creates a new HTTP service that serves REST endpoints.
26func NewHTTPService(socketPath string, cmd *Command) *HTTPService {
27 return &HTTPService{
28 cmd: cmd,
29 socketPath: socketPath,
30 }
31}
32
33// Start starts the HTTP server.
34func (h *HTTPService) Start() error {
35 mux := h.HTTPMux()
36
37 // Ensure parent dir exists
38 if err := os.MkdirAll(filepath.Dir(h.socketPath), 0700); err != nil {
39 return fmt.Errorf("creating socket dir: %w", err)
40 }
41 // Remove stale socket from ungraceful shutdown
42 _ = os.Remove(h.socketPath)
43
44 listener, err := net.Listen("unix", h.socketPath)
45 if err != nil {
46 return fmt.Errorf("listening on unix socket: %w", err)
47 }
48 if err := os.Chmod(h.socketPath, 0660); err != nil {
49 return fmt.Errorf("setting socket permissions: %w", err)
50 }
51
52 h.srv = &http.Server{
53 Handler: mux,
54 ReadHeaderTimeout: 5 * time.Second,
55 ReadTimeout: 10 * time.Second,
56 WriteTimeout: 30 * time.Second,
57 IdleTimeout: 120 * time.Second,
58 }
59
60 slog.Info("starting HTTP server on unix socket", "path", h.socketPath)
61 err = h.srv.Serve(listener)
62 if err != nil && !errors.Is(err, http.ErrServerClosed) {
63 return err
64 }
65 return nil
66}
67
68// Stop stops the HTTP server gracefully.
69func (h *HTTPService) Stop() error {
70 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
71 defer cancel()
72
73 slog.Info("shutting down HTTP server")
74 return h.srv.Shutdown(ctx)
75}
76
77// Status returns the status of the HTTP service.
78func (h *HTTPService) Status() string {
79 if h.srv != nil {
80 return "running"
81 }
82 return "stopped"
83}
84
85// HTTPMux creates and returns the HTTP ServeMux with all routes.
86func (h *HTTPService) HTTPMux() *http.ServeMux {
87 mux := http.NewServeMux()
88
89 // User endpoints
90 mux.HandleFunc("GET /users", h.listUsers)
91 mux.HandleFunc("POST /users", h.createUser)
92 mux.HandleFunc("DELETE /users/{username}", h.deleteUser)
93 mux.HandleFunc("POST /users/{username}/addresses", h.addUserAddress)
94
95 // SMTP auth endpoints
96 mux.HandleFunc("GET /smtp-auth", h.listSMTPAuth)
97 mux.HandleFunc("POST /smtp-auth", h.createSMTPAuth)
98 mux.HandleFunc("DELETE /smtp-auth/{name}", h.deleteSMTPAuth)
99
100 // DKIM endpoints
101 mux.HandleFunc("GET /dkim", h.listDKIM)
102 mux.HandleFunc("POST /dkim", h.addDKIM)
103 mux.HandleFunc("GET /dkim/{selector}/{domain}", h.getDKIM)
104 mux.HandleFunc("POST /dkim/{selector}/{domain}/enable", h.enableDKIM)
105 mux.HandleFunc("POST /dkim/{selector}/{domain}/disable", h.disableDKIM)
106 mux.HandleFunc("DELETE /dkim/{selector}/{domain}", h.deleteDKIM)
107
108 // Prune endpoint
109 mux.HandleFunc("POST /prune", h.pruneMails)
110
111 return mux
112}
113
114// Helper to write JSON responses
115func writeJSON(w http.ResponseWriter, status int, v interface{}) {
116 w.Header().Set("Content-Type", "application/json")
117 w.WriteHeader(status)
118 if err := json.NewEncoder(w).Encode(v); err != nil {
119 slog.Error("failed to encode JSON response", "error", err)
120 }
121}
122
123// Helper to write error responses
124func writeError(w http.ResponseWriter, err error) {
125 switch {
126 case errors.Is(err, model.ErrAlreadyExists):
127 writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
128 case errors.Is(err, model.ErrNotFound):
129 writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
130 case errors.Is(err, model.ErrInvalidInput):
131 writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
132 default:
133 writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
134 }
135}
136
137func (h *HTTPService) listUsers(w http.ResponseWriter, r *http.Request) {
138 users, err := h.cmd.ListUsers(r.Context())
139 if err != nil {
140 writeError(w, err)
141 return
142 }
143 writeJSON(w, http.StatusOK, users)
144}
145
146func (h *HTTPService) createUser(w http.ResponseWriter, r *http.Request) {
147 var req struct {
148 Username string `json:"username"`
149 }
150 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
151 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
152 return
153 }
154 if req.Username == "" {
155 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
156 return
157 }
158
159 user, err := h.cmd.CreateUser(r.Context(), req.Username)
160 if err != nil {
161 writeError(w, err)
162 return
163 }
164
165 writeJSON(w, http.StatusCreated, user)
166}
167
168func (h *HTTPService) deleteUser(w http.ResponseWriter, r *http.Request) {
169 username := r.PathValue("username")
170 if username == "" {
171 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
172 return
173 }
174
175 err := h.cmd.RemoveUser(r.Context(), username)
176 if err != nil {
177 writeError(w, err)
178 return
179 }
180
181 w.WriteHeader(http.StatusNoContent)
182}
183
184func (h *HTTPService) addUserAddress(w http.ResponseWriter, r *http.Request) {
185 username := r.PathValue("username")
186 if username == "" {
187 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
188 return
189 }
190
191 var req struct {
192 Address string `json:"address"`
193 }
194 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
195 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
196 return
197 }
198 if req.Address == "" {
199 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "address is required"})
200 return
201 }
202
203 err := h.cmd.AddUserAddress(r.Context(), username, req.Address)
204 if err != nil {
205 writeError(w, err)
206 return
207 }
208
209 w.WriteHeader(http.StatusCreated)
210}
211
212func (h *HTTPService) listSMTPAuth(w http.ResponseWriter, r *http.Request) {
213 auths, err := h.cmd.ListSMTPAuth(r.Context())
214 if err != nil {
215 writeError(w, err)
216 return
217 }
218 writeJSON(w, http.StatusOK, auths)
219}
220
221func (h *HTTPService) createSMTPAuth(w http.ResponseWriter, r *http.Request) {
222 var req struct {
223 Name string `json:"name"`
224 }
225 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
226 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
227 return
228 }
229 if req.Name == "" {
230 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
231 return
232 }
233
234 auth, err := h.cmd.CreateSMTPAuth(r.Context(), req.Name)
235 if err != nil {
236 writeError(w, err)
237 return
238 }
239
240 writeJSON(w, http.StatusCreated, auth)
241}
242
243func (h *HTTPService) deleteSMTPAuth(w http.ResponseWriter, r *http.Request) {
244 name := r.PathValue("name")
245 if name == "" {
246 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
247 return
248 }
249
250 err := h.cmd.RemoveSMTPAuth(r.Context(), name)
251 if err != nil {
252 writeError(w, err)
253 return
254 }
255
256 w.WriteHeader(http.StatusNoContent)
257}
258
259func (h *HTTPService) listDKIM(w http.ResponseWriter, r *http.Request) {
260 dkims, err := h.cmd.ListDKIM(r.Context())
261 if err != nil {
262 writeError(w, err)
263 return
264 }
265 writeJSON(w, http.StatusOK, dkims)
266}
267
268func (h *HTTPService) addDKIM(w http.ResponseWriter, r *http.Request) {
269 var req struct {
270 Selector string `json:"selector"`
271 Domain string `json:"domain"`
272 }
273 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
274 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
275 return
276 }
277 if req.Selector == "" {
278 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
279 return
280 }
281 if req.Domain == "" {
282 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
283 return
284 }
285
286 record, err := h.cmd.AddDKIM(r.Context(), req.Selector, req.Domain)
287 if err != nil {
288 writeError(w, err)
289 return
290 }
291
292 writeJSON(w, http.StatusOK, map[string]string{"dns_record": record})
293}
294
295func (h *HTTPService) getDKIM(w http.ResponseWriter, r *http.Request) {
296 selector := r.PathValue("selector")
297 domain := r.PathValue("domain")
298 if selector == "" {
299 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
300 return
301 }
302 if domain == "" {
303 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
304 return
305 }
306
307 record, err := h.cmd.GetDKIM(r.Context(), selector, domain)
308 if err != nil {
309 writeError(w, err)
310 return
311 }
312
313 writeJSON(w, http.StatusOK, map[string]string{"dns_record": record})
314}
315
316func (h *HTTPService) enableDKIM(w http.ResponseWriter, r *http.Request) {
317 selector := r.PathValue("selector")
318 domain := r.PathValue("domain")
319 if selector == "" {
320 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
321 return
322 }
323 if domain == "" {
324 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
325 return
326 }
327
328 // Parse force query parameter
329 forceStr := r.URL.Query().Get("force")
330 force := forceStr != "false" // Default to true if not explicitly set to "false"
331
332 err := h.cmd.SetDKIMEnabled(r.Context(), selector, domain, true, force)
333 if err != nil {
334 writeError(w, err)
335 return
336 }
337
338 w.WriteHeader(http.StatusNoContent)
339}
340
341func (h *HTTPService) disableDKIM(w http.ResponseWriter, r *http.Request) {
342 selector := r.PathValue("selector")
343 domain := r.PathValue("domain")
344 if selector == "" {
345 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
346 return
347 }
348 if domain == "" {
349 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
350 return
351 }
352
353 err := h.cmd.SetDKIMEnabled(r.Context(), selector, domain, false, true)
354 if err != nil {
355 writeError(w, err)
356 return
357 }
358
359 w.WriteHeader(http.StatusNoContent)
360}
361
362func (h *HTTPService) deleteDKIM(w http.ResponseWriter, r *http.Request) {
363 selector := r.PathValue("selector")
364 domain := r.PathValue("domain")
365 if selector == "" {
366 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
367 return
368 }
369 if domain == "" {
370 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
371 return
372 }
373
374 err := h.cmd.RemoveDKIM(r.Context(), selector, domain)
375 if err != nil {
376 writeError(w, err)
377 return
378 }
379
380 w.WriteHeader(http.StatusNoContent)
381}
382
383func (h *HTTPService) pruneMails(w http.ResponseWriter, r *http.Request) {
384 count, err := h.cmd.PruneMails(r.Context())
385 if err != nil {
386 writeError(w, err)
387 return
388 }
389
390 writeJSON(w, http.StatusOK, map[string]int{"pruned": count})
391}