all repos — postern

@ 50dc541db37adabbbb220f4abf62aba899c48f5d

Modern mail management

clone
git clone https:///postern.git

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	// KV endpoints
112	mux.HandleFunc("GET /kv", h.kvList)
113	mux.HandleFunc("GET /kv/{key...}", h.kvGet)
114	mux.HandleFunc("DELETE /kv/{key...}", h.kvPop)
115
116	return mux
117}
118
119// Helper to write JSON responses
120func writeJSON(w http.ResponseWriter, status int, v interface{}) {
121	w.Header().Set("Content-Type", "application/json")
122	w.WriteHeader(status)
123	if err := json.NewEncoder(w).Encode(v); err != nil {
124		slog.Error("failed to encode JSON response", "error", err)
125	}
126}
127
128// Helper to write error responses
129func writeError(w http.ResponseWriter, err error) {
130	switch {
131	case errors.Is(err, model.ErrAlreadyExists):
132		writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
133	case errors.Is(err, model.ErrNotFound):
134		writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
135	case errors.Is(err, model.ErrInvalidInput):
136		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
137	default:
138		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
139	}
140}
141
142func (h *HTTPService) listUsers(w http.ResponseWriter, r *http.Request) {
143	users, err := h.cmd.ListUsers(r.Context())
144	if err != nil {
145		writeError(w, err)
146		return
147	}
148	writeJSON(w, http.StatusOK, users)
149}
150
151func (h *HTTPService) createUser(w http.ResponseWriter, r *http.Request) {
152	var req struct {
153		Username string `json:"username"`
154	}
155	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
156		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
157		return
158	}
159	if req.Username == "" {
160		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
161		return
162	}
163
164	user, err := h.cmd.CreateUser(r.Context(), req.Username)
165	if err != nil {
166		writeError(w, err)
167		return
168	}
169
170	writeJSON(w, http.StatusCreated, user)
171}
172
173func (h *HTTPService) deleteUser(w http.ResponseWriter, r *http.Request) {
174	username := r.PathValue("username")
175	if username == "" {
176		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
177		return
178	}
179
180	err := h.cmd.RemoveUser(r.Context(), username)
181	if err != nil {
182		writeError(w, err)
183		return
184	}
185
186	w.WriteHeader(http.StatusNoContent)
187}
188
189func (h *HTTPService) addUserAddress(w http.ResponseWriter, r *http.Request) {
190	username := r.PathValue("username")
191	if username == "" {
192		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"})
193		return
194	}
195
196	var req struct {
197		Address string `json:"address"`
198	}
199	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
200		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
201		return
202	}
203	if req.Address == "" {
204		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "address is required"})
205		return
206	}
207
208	err := h.cmd.AddUserAddress(r.Context(), username, req.Address)
209	if err != nil {
210		writeError(w, err)
211		return
212	}
213
214	w.WriteHeader(http.StatusCreated)
215}
216
217func (h *HTTPService) listSMTPAuth(w http.ResponseWriter, r *http.Request) {
218	auths, err := h.cmd.ListSMTPAuth(r.Context())
219	if err != nil {
220		writeError(w, err)
221		return
222	}
223	writeJSON(w, http.StatusOK, auths)
224}
225
226func (h *HTTPService) createSMTPAuth(w http.ResponseWriter, r *http.Request) {
227	var req struct {
228		Name string `json:"name"`
229	}
230	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
231		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
232		return
233	}
234	if req.Name == "" {
235		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
236		return
237	}
238
239	auth, err := h.cmd.CreateSMTPAuth(r.Context(), req.Name)
240	if err != nil {
241		writeError(w, err)
242		return
243	}
244
245	writeJSON(w, http.StatusCreated, auth)
246}
247
248func (h *HTTPService) deleteSMTPAuth(w http.ResponseWriter, r *http.Request) {
249	name := r.PathValue("name")
250	if name == "" {
251		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
252		return
253	}
254
255	err := h.cmd.RemoveSMTPAuth(r.Context(), name)
256	if err != nil {
257		writeError(w, err)
258		return
259	}
260
261	w.WriteHeader(http.StatusNoContent)
262}
263
264func (h *HTTPService) listDKIM(w http.ResponseWriter, r *http.Request) {
265	dkims, err := h.cmd.ListDKIM(r.Context())
266	if err != nil {
267		writeError(w, err)
268		return
269	}
270	writeJSON(w, http.StatusOK, dkims)
271}
272
273func (h *HTTPService) addDKIM(w http.ResponseWriter, r *http.Request) {
274	var req struct {
275		Selector string `json:"selector"`
276		Domain   string `json:"domain"`
277	}
278	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
279		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
280		return
281	}
282	if req.Selector == "" {
283		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
284		return
285	}
286	if req.Domain == "" {
287		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
288		return
289	}
290
291	record, err := h.cmd.AddDKIM(r.Context(), req.Selector, req.Domain)
292	if err != nil {
293		writeError(w, err)
294		return
295	}
296
297	writeJSON(w, http.StatusOK, map[string]string{"dns_record": record})
298}
299
300func (h *HTTPService) getDKIM(w http.ResponseWriter, r *http.Request) {
301	selector := r.PathValue("selector")
302	domain := r.PathValue("domain")
303	if selector == "" {
304		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
305		return
306	}
307	if domain == "" {
308		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
309		return
310	}
311
312	record, err := h.cmd.GetDKIM(r.Context(), selector, domain)
313	if err != nil {
314		writeError(w, err)
315		return
316	}
317
318	writeJSON(w, http.StatusOK, map[string]string{"dns_record": record})
319}
320
321func (h *HTTPService) enableDKIM(w http.ResponseWriter, r *http.Request) {
322	selector := r.PathValue("selector")
323	domain := r.PathValue("domain")
324	if selector == "" {
325		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
326		return
327	}
328	if domain == "" {
329		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
330		return
331	}
332
333	// Parse force query parameter
334	forceStr := r.URL.Query().Get("force")
335	force := forceStr != "false" // Default to true if not explicitly set to "false"
336
337	err := h.cmd.SetDKIMEnabled(r.Context(), selector, domain, true, force)
338	if err != nil {
339		writeError(w, err)
340		return
341	}
342
343	w.WriteHeader(http.StatusNoContent)
344}
345
346func (h *HTTPService) disableDKIM(w http.ResponseWriter, r *http.Request) {
347	selector := r.PathValue("selector")
348	domain := r.PathValue("domain")
349	if selector == "" {
350		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
351		return
352	}
353	if domain == "" {
354		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
355		return
356	}
357
358	err := h.cmd.SetDKIMEnabled(r.Context(), selector, domain, false, true)
359	if err != nil {
360		writeError(w, err)
361		return
362	}
363
364	w.WriteHeader(http.StatusNoContent)
365}
366
367func (h *HTTPService) deleteDKIM(w http.ResponseWriter, r *http.Request) {
368	selector := r.PathValue("selector")
369	domain := r.PathValue("domain")
370	if selector == "" {
371		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "selector is required"})
372		return
373	}
374	if domain == "" {
375		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"})
376		return
377	}
378
379	err := h.cmd.RemoveDKIM(r.Context(), selector, domain)
380	if err != nil {
381		writeError(w, err)
382		return
383	}
384
385	w.WriteHeader(http.StatusNoContent)
386}
387
388func (h *HTTPService) pruneMails(w http.ResponseWriter, r *http.Request) {
389	count, err := h.cmd.PruneMails(r.Context())
390	if err != nil {
391		writeError(w, err)
392		return
393	}
394
395	writeJSON(w, http.StatusOK, map[string]int{"pruned": count})
396}
397
398func (h *HTTPService) kvList(w http.ResponseWriter, r *http.Request) {
399	pairs, err := h.cmd.KVList(r.Context())
400	if err != nil {
401		writeError(w, err)
402		return
403	}
404	writeJSON(w, http.StatusOK, pairs)
405}
406
407func (h *HTTPService) kvGet(w http.ResponseWriter, r *http.Request) {
408	key := r.PathValue("key")
409	if key == "" {
410		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "key is required"})
411		return
412	}
413
414	val, err := h.cmd.KVGet(r.Context(), key)
415	if err != nil {
416		writeError(w, err)
417		return
418	}
419
420	writeJSON(w, http.StatusOK, map[string]any{"value": val})
421}
422
423func (h *HTTPService) kvPop(w http.ResponseWriter, r *http.Request) {
424	key := r.PathValue("key")
425	if key == "" {
426		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "key is required"})
427		return
428	}
429
430	val, err := h.cmd.KVPop(r.Context(), key)
431	if err != nil {
432		writeError(w, err)
433		return
434	}
435
436	writeJSON(w, http.StatusOK, map[string]any{"value": val})
437}