all repos — postern

@ 9799a3215ac0185fb9b4e7d6ec0645c2cb2ce55b

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	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}