internal/cli/client.go (view raw)
1package cli
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "net"
9 "net/http"
10 "net/url"
11
12 "postern/internal/model"
13)
14
15func newUnixHTTPClient(socketPath string) *http.Client {
16 return &http.Client{
17 Transport: &http.Transport{
18 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
19 var d net.Dialer
20 return d.DialContext(ctx, "unix", socketPath)
21 },
22 },
23 }
24}
25
26// clientListUsers fetches the list of users from the daemon.
27func (c *Command) clientListUsers(ctx context.Context) (_ []model.User, err error) {
28 resp, err := c.httpClient.Get("http://unix/users")
29 if err != nil {
30 return nil, fmt.Errorf("failed to list users: %w", err)
31 }
32 defer model.Wrap(resp.Body.Close, &err, "clientListUsers closing response body")
33
34 if resp.StatusCode != http.StatusOK {
35 return nil, fmt.Errorf("failed to list users: %d", resp.StatusCode)
36 }
37
38 var users []model.User
39 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
40 return nil, fmt.Errorf("failed to decode users response: %w", err)
41 }
42
43 return users, nil
44}
45
46// clientCreateUser creates a new user via the daemon.
47func (c *Command) clientCreateUser(ctx context.Context, username string) (_ model.User, err error) {
48 reqBody := map[string]string{"username": username}
49 body, err := json.Marshal(reqBody)
50 if err != nil {
51 return model.User{}, fmt.Errorf("failed to marshal user request: %w", err)
52 }
53
54 resp, err := c.httpClient.Post("http://unix/users", "application/json", bytes.NewReader(body))
55 if err != nil {
56 return model.User{}, fmt.Errorf("failed to create user: %w", err)
57 }
58 defer model.Wrap(resp.Body.Close, &err, "clientCreateUser closing response body")
59
60 if resp.StatusCode != http.StatusCreated {
61 return model.User{}, fmt.Errorf("failed to create user: %d", resp.StatusCode)
62 }
63
64 var user model.User
65 if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
66 return model.User{}, fmt.Errorf("failed to decode user response: %w", err)
67 }
68
69 return user, nil
70}
71
72// clientRotateUserPassword rotates a user's password via the daemon.
73func (c *Command) clientRotateUserPassword(ctx context.Context, username string) (_ model.User, err error) {
74 path, err := url.JoinPath("http://unix/users", username, "rotate-password")
75 if err != nil {
76 return model.User{}, fmt.Errorf("failed to create rotate password request path: %w", err)
77 }
78
79 resp, err := c.httpClient.Post(path, "application/json", nil)
80 if err != nil {
81 return model.User{}, fmt.Errorf("failed to rotate user password: %w", err)
82 }
83 defer model.Wrap(resp.Body.Close, &err, "clientRotateUserPassword closing response body")
84
85 if resp.StatusCode != http.StatusOK {
86 return model.User{}, fmt.Errorf("failed to rotate user password: %d", resp.StatusCode)
87 }
88
89 var user model.User
90 if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
91 return model.User{}, fmt.Errorf("failed to decode user response: %w", err)
92 }
93
94 return user, nil
95}
96
97// clientRemoveUser removes a user via the daemon.
98func (c *Command) clientRemoveUser(ctx context.Context, username string) (err error) {
99 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, "http://unix/users/"+username, nil)
100 if err != nil {
101 return fmt.Errorf("failed to create delete user request: %w", err)
102 }
103
104 resp, err := c.httpClient.Do(req)
105 if err != nil {
106 return fmt.Errorf("failed to remove user: %w", err)
107 }
108 defer model.Wrap(resp.Body.Close, &err, "clientRemoveUser closing response body")
109
110 if resp.StatusCode != http.StatusNoContent {
111 return fmt.Errorf("failed to remove user: %d", resp.StatusCode)
112 }
113
114 return nil
115}
116
117// clientAddUserAddress adds an address for a user via the daemon.
118func (c *Command) clientAddUserAddress(ctx context.Context, username, address string) error {
119 reqBody := map[string]string{"address": address}
120 body, err := json.Marshal(reqBody)
121 if err != nil {
122 return fmt.Errorf("failed to marshal user address request: %w", err)
123 }
124
125 path, err := url.JoinPath("http://unix/users/", username, "addresses")
126 if err != nil {
127 return fmt.Errorf("failed to create address request: %w", err)
128 }
129
130 resp, err := c.httpClient.Post(path, "application/json", bytes.NewReader(body))
131 if err != nil {
132 return fmt.Errorf("failed to add user address: %w", err)
133 }
134 defer model.Wrap(resp.Body.Close, &err, "clientAddUserAddress closing response body")
135
136 if resp.StatusCode != http.StatusCreated {
137 return fmt.Errorf("failed to add user address: %d", resp.StatusCode)
138 }
139
140 return nil
141}
142
143// clientRemoveUserAddress removes an address for a user via the daemon.
144func (c *Command) clientRemoveUserAddress(ctx context.Context, username, address string) (err error) {
145 path, err := url.JoinPath("http://unix/users/", username, "addresses", address)
146 if err != nil {
147 return fmt.Errorf("failed to create address request: %w", err)
148 }
149
150 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil)
151 if err != nil {
152 return fmt.Errorf("failed to create delete address request: %w", err)
153 }
154
155 resp, err := c.httpClient.Do(req)
156 if err != nil {
157 return fmt.Errorf("failed to remove user address: %w", err)
158 }
159 defer model.Wrap(resp.Body.Close, &err, "clientRemoveUserAddress closing response body")
160
161 if resp.StatusCode != http.StatusNoContent {
162 return fmt.Errorf("failed to remove user address: %d", resp.StatusCode)
163 }
164
165 return nil
166}
167
168// clientListUserAddresses fetches the list of addresses for a user from the daemon.
169func (c *Command) clientListUserAddresses(ctx context.Context, username string) (_ []string, err error) {
170 path, err := url.JoinPath("http://unix/users/", username, "addresses")
171 if err != nil {
172 return nil, fmt.Errorf("failed to create address request: %w", err)
173 }
174
175 resp, err := c.httpClient.Get(path)
176 if err != nil {
177 return nil, fmt.Errorf("failed to list user addresses: %w", err)
178 }
179 defer model.Wrap(resp.Body.Close, &err, "clientListUserAddresses closing response body")
180
181 if resp.StatusCode != http.StatusOK {
182 return nil, fmt.Errorf("failed to list user addresses: %d", resp.StatusCode)
183 }
184
185 var addresses []string
186 if err := json.NewDecoder(resp.Body).Decode(&addresses); err != nil {
187 return nil, fmt.Errorf("failed to decode user addresses response: %w", err)
188 }
189
190 return addresses, nil
191}
192
193// clientListSMTPAuth fetches the list of SMTP auth users from the daemon.
194func (c *Command) clientListSMTPAuth(ctx context.Context) ([]model.SMTPAuth, error) {
195 resp, err := c.httpClient.Get("http://unix/smtp-auth")
196 if err != nil {
197 return nil, fmt.Errorf("failed to list smtp auth: %w", err)
198 }
199 defer model.Wrap(resp.Body.Close, &err, "clientListSMTPAuth closing response body")
200
201 if resp.StatusCode != http.StatusOK {
202 return nil, fmt.Errorf("failed to list smtp auth: %d", resp.StatusCode)
203 }
204
205 var auths []model.SMTPAuth
206 if err := json.NewDecoder(resp.Body).Decode(&auths); err != nil {
207 return nil, fmt.Errorf("failed to decode smtp auth response: %w", err)
208 }
209
210 return auths, nil
211}
212
213// clientCreateSMTPAuth creates a new SMTP auth user via the daemon.
214func (c *Command) clientCreateSMTPAuth(_ context.Context, name string) (model.SMTPAuth, error) {
215 reqBody := map[string]string{"name": name}
216 body, err := json.Marshal(reqBody)
217 if err != nil {
218 return model.SMTPAuth{}, fmt.Errorf("failed to marshal smtp auth request: %w", err)
219 }
220
221 resp, err := c.httpClient.Post("http://unix/smtp-auth", "application/json", bytes.NewReader(body))
222 if err != nil {
223 return model.SMTPAuth{}, fmt.Errorf("failed to create smtp auth: %w", err)
224 }
225 defer model.Wrap(resp.Body.Close, &err, "clientCreateSMTPAuth closing response body")
226
227 if resp.StatusCode != http.StatusCreated {
228 return model.SMTPAuth{}, fmt.Errorf("failed to create smtp auth: %d", resp.StatusCode)
229 }
230
231 var auth model.SMTPAuth
232 if err := json.NewDecoder(resp.Body).Decode(&auth); err != nil {
233 return model.SMTPAuth{}, fmt.Errorf("failed to decode smtp auth response: %w", err)
234 }
235
236 return auth, nil
237}
238
239// clientRemoveSMTPAuth removes an SMTP auth user via the daemon.
240func (c *Command) clientRemoveSMTPAuth(ctx context.Context, name string) error {
241 path, err := url.JoinPath("http://unix/smtp-auth", name)
242 if err != nil {
243 return fmt.Errorf("failed to create remove smtp auth request path: %w", err)
244 }
245 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil)
246 if err != nil {
247 return fmt.Errorf("failed to create delete smtp auth request: %w", err)
248 }
249
250 resp, err := c.httpClient.Do(req)
251 if err != nil {
252 return fmt.Errorf("failed to remove smtp auth: %w", err)
253 }
254 defer model.Wrap(resp.Body.Close, &err, "clientRemoveSMTPAuth closing response body")
255
256 if resp.StatusCode != http.StatusNoContent {
257 return fmt.Errorf("failed to remove smtp auth: %d", resp.StatusCode)
258 }
259
260 return nil
261}
262
263// clientListDKIM fetches the list of DKIM selectors from the daemon.
264func (c *Command) clientListDKIM(ctx context.Context) ([]model.DKIM, error) {
265 resp, err := c.httpClient.Get("http://unix/dkim")
266 if err != nil {
267 return nil, fmt.Errorf("failed to list dkim: %w", err)
268 }
269 defer model.Wrap(resp.Body.Close, &err, "clientListDKIM closing response body")
270
271 if resp.StatusCode != http.StatusOK {
272 return nil, fmt.Errorf("failed to list dkim: %d", resp.StatusCode)
273 }
274
275 var dkims []model.DKIM
276 if err := json.NewDecoder(resp.Body).Decode(&dkims); err != nil {
277 return nil, fmt.Errorf("failed to decode dkim response: %w", err)
278 }
279
280 return dkims, nil
281}
282
283// clientAddDKIM adds a new DKIM selector via the daemon.
284func (c *Command) clientAddDKIM(ctx context.Context, selector, domain string) (string, error) {
285 reqBody := map[string]string{"selector": selector, "domain": domain}
286 body, err := json.Marshal(reqBody)
287 if err != nil {
288 return "", fmt.Errorf("failed to marshal dkim request: %w", err)
289 }
290
291 resp, err := c.httpClient.Post("http://unix/dkim", "application/json", bytes.NewReader(body))
292 if err != nil {
293 return "", fmt.Errorf("failed to add dkim: %w", err)
294 }
295 defer model.Wrap(resp.Body.Close, &err, "clientAddDKIM closing response body")
296
297 if resp.StatusCode != http.StatusOK {
298 return "", fmt.Errorf("failed to add dkim: %d", resp.StatusCode)
299 }
300
301 var result map[string]string
302 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
303 return "", fmt.Errorf("failed to decode dkim response: %w", err)
304 }
305
306 return result["dns_record"], nil
307}
308
309// clientGetDKIM gets the public DNS record for a DKIM selector via the daemon.
310func (c *Command) clientGetDKIM(_ context.Context, selector, domain string) (string, error) {
311 path, err := url.JoinPath("http://unix/dkim", selector, domain)
312 if err != nil {
313 return "", fmt.Errorf("failed to create dkim request path: %w", err)
314 }
315 resp, err := c.httpClient.Get(path)
316 if err != nil {
317 return "", fmt.Errorf("failed to get dkim: %w", err)
318 }
319 defer model.Wrap(resp.Body.Close, &err, "clientGetDKIM closing response body")
320
321 if resp.StatusCode != http.StatusOK {
322 return "", fmt.Errorf("failed to get dkim: %d", resp.StatusCode)
323 }
324
325 var result map[string]string
326 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
327 return "", fmt.Errorf("failed to decode dkim response: %w", err)
328 }
329
330 return result["dns_record"], nil
331}
332
333// clientSetDKIMEnabled enables/disables a DKIM selector via the daemon.
334func (c *Command) clientSetDKIMEnabled(_ context.Context, selector, domain string, enable bool, force bool) error {
335 enableKeyword := "enable"
336 if !enable {
337 enableKeyword = "disable"
338 }
339
340 path, err := url.JoinPath("http://unix/dkim", selector, domain, enableKeyword)
341 if err != nil {
342 return fmt.Errorf("failed to create dkim request path: %w", err)
343 }
344
345 // Include force parameter in query string
346 if !force {
347 path += "?force=false"
348 }
349
350 resp, err := c.httpClient.Post(path, "application/json", nil)
351 if err != nil {
352 return fmt.Errorf("failed to set dkim enabled: %w", err)
353 }
354 defer model.Wrap(resp.Body.Close, &err, "clientSetDKIMEnabled closing response body")
355
356 if resp.StatusCode != http.StatusNoContent {
357 return fmt.Errorf("failed to set dkim enabled: %d", resp.StatusCode)
358 }
359
360 return nil
361}
362
363// clientRemoveDKIM removes a DKIM selector via the daemon.
364func (c *Command) clientRemoveDKIM(ctx context.Context, selector, domain string) error {
365 path, err := url.JoinPath("http://unix/dkim", selector, domain)
366 if err != nil {
367 return fmt.Errorf("failed to create dkim request path: %w", err)
368 }
369 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil)
370 if err != nil {
371 return fmt.Errorf("failed to create delete dkim request: %w", err)
372 }
373
374 resp, err := c.httpClient.Do(req)
375 if err != nil {
376 return fmt.Errorf("failed to remove dkim: %w", err)
377 }
378 defer model.Wrap(resp.Body.Close, &err, "clientRemoveDKIM closing response body")
379
380 if resp.StatusCode != http.StatusNoContent {
381 return fmt.Errorf("failed to remove dkim: %d", resp.StatusCode)
382 }
383
384 return nil
385}
386
387// clientPruneMails prunes orphaned mails via the daemon.
388func (c *Command) clientPruneMails(_ context.Context) (int, error) {
389 resp, err := c.httpClient.Post("http://unix/prune", "application/json", nil)
390 if err != nil {
391 return 0, fmt.Errorf("failed to prune mails: %w", err)
392 }
393 defer model.Wrap(resp.Body.Close, &err, "clientPruneMails closing response body")
394
395 if resp.StatusCode != http.StatusOK {
396 return 0, fmt.Errorf("failed to prune mails: %d", resp.StatusCode)
397 }
398
399 var result map[string]int
400 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
401 return 0, fmt.Errorf("failed to decode prune response: %w", err)
402 }
403
404 return result["pruned"], nil
405}
406
407// clientKVList fetches all key-value pairs from the daemon.
408func (c *Command) clientKVList(_ context.Context) (map[string]any, error) {
409 resp, err := c.httpClient.Get("http://unix/kv")
410 if err != nil {
411 return nil, fmt.Errorf("failed to list kv: %w", err)
412 }
413 defer model.Wrap(resp.Body.Close, &err, "clientKVList closing response body")
414
415 if resp.StatusCode != http.StatusOK {
416 return nil, fmt.Errorf("failed to list kv: %d", resp.StatusCode)
417 }
418
419 var pairs map[string]any
420 if err := json.NewDecoder(resp.Body).Decode(&pairs); err != nil {
421 return nil, fmt.Errorf("failed to decode kv response: %w", err)
422 }
423
424 return pairs, nil
425}
426
427// clientKVGet fetches the value stored under key from the daemon.
428// The key is URL-encoded so keys containing slashes work.
429func (c *Command) clientKVGet(_ context.Context, key string) (any, error) {
430 path := "http://unix/kv/" + url.PathEscape(key)
431
432 resp, err := c.httpClient.Get(path)
433 if err != nil {
434 return nil, fmt.Errorf("failed to get kv: %w", err)
435 }
436 defer model.Wrap(resp.Body.Close, &err, "clientKVGet closing response body")
437
438 if resp.StatusCode != http.StatusOK {
439 return nil, fmt.Errorf("failed to get kv: %d", resp.StatusCode)
440 }
441
442 var result struct {
443 Value any `json:"value"`
444 }
445 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
446 return nil, fmt.Errorf("failed to decode kv response: %w", err)
447 }
448
449 return result.Value, nil
450}
451
452// clientKVPop removes the value stored under key via the daemon and returns it.
453// The key is URL-encoded so keys containing slashes work.
454func (c *Command) clientKVPop(ctx context.Context, key string) (any, error) {
455 path := "http://unix/kv/" + url.PathEscape(key)
456 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil)
457 if err != nil {
458 return nil, fmt.Errorf("failed to create pop kv request: %w", err)
459 }
460
461 resp, err := c.httpClient.Do(req)
462 if err != nil {
463 return nil, fmt.Errorf("failed to pop kv: %w", err)
464 }
465 defer model.Wrap(resp.Body.Close, &err, "clientKVPop closing response body")
466
467 if resp.StatusCode != http.StatusOK {
468 return nil, fmt.Errorf("failed to pop kv: %d", resp.StatusCode)
469 }
470
471 var result struct {
472 Value any `json:"value"`
473 }
474 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
475 return nil, fmt.Errorf("failed to decode kv response: %w", err)
476 }
477
478 return result.Value, nil
479}