all repos — postern

@ 69d75eed3ba35592ffe87ee92c5e52e1f6deafce

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