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// clientRemoveUser removes a user via the daemon.
73func (c *Command) clientRemoveUser(ctx context.Context, username string) (err error) {
74 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, "http://unix/users/"+username, nil)
75 if err != nil {
76 return fmt.Errorf("failed to create delete user request: %w", err)
77 }
78
79 resp, err := c.httpClient.Do(req)
80 if err != nil {
81 return fmt.Errorf("failed to remove user: %w", err)
82 }
83 defer model.Wrap(resp.Body.Close, &err, "clientRemoveUser closing response body")
84
85 if resp.StatusCode != http.StatusNoContent {
86 return fmt.Errorf("failed to remove user: %d", resp.StatusCode)
87 }
88
89 return nil
90}
91
92// clientAddUserAddress adds an address for a user via the daemon.
93func (c *Command) clientAddUserAddress(ctx context.Context, username, address string) error {
94 reqBody := map[string]string{"address": address}
95 body, err := json.Marshal(reqBody)
96 if err != nil {
97 return fmt.Errorf("failed to marshal user address request: %w", err)
98 }
99
100 path, err := url.JoinPath("http://unix/users/", username, "addresses")
101 if err != nil {
102 return fmt.Errorf("failed to create address request: %w", err)
103 }
104
105 resp, err := c.httpClient.Post(path, "application/json", bytes.NewReader(body))
106 if err != nil {
107 return fmt.Errorf("failed to add user address: %w", err)
108 }
109 defer model.Wrap(resp.Body.Close, &err, "clientAddUserAddress closing response body")
110
111 if resp.StatusCode != http.StatusCreated {
112 return fmt.Errorf("failed to add user address: %d", resp.StatusCode)
113 }
114
115 return nil
116}
117
118// clientListSMTPAuth fetches the list of SMTP auth users from the daemon.
119func (c *Command) clientListSMTPAuth(ctx context.Context) ([]model.SMTPAuth, error) {
120 resp, err := c.httpClient.Get("http://unix/smtp-auth")
121 if err != nil {
122 return nil, fmt.Errorf("failed to list smtp auth: %w", err)
123 }
124 defer model.Wrap(resp.Body.Close, &err, "clientListSMTPAuth closing response body")
125
126 if resp.StatusCode != http.StatusOK {
127 return nil, fmt.Errorf("failed to list smtp auth: %d", resp.StatusCode)
128 }
129
130 var auths []model.SMTPAuth
131 if err := json.NewDecoder(resp.Body).Decode(&auths); err != nil {
132 return nil, fmt.Errorf("failed to decode smtp auth response: %w", err)
133 }
134
135 return auths, nil
136}
137
138// clientCreateSMTPAuth creates a new SMTP auth user via the daemon.
139func (c *Command) clientCreateSMTPAuth(_ context.Context, name string) (model.SMTPAuth, error) {
140 reqBody := map[string]string{"name": name}
141 body, err := json.Marshal(reqBody)
142 if err != nil {
143 return model.SMTPAuth{}, fmt.Errorf("failed to marshal smtp auth request: %w", err)
144 }
145
146 resp, err := c.httpClient.Post("http://unix/smtp-auth", "application/json", bytes.NewReader(body))
147 if err != nil {
148 return model.SMTPAuth{}, fmt.Errorf("failed to create smtp auth: %w", err)
149 }
150 defer model.Wrap(resp.Body.Close, &err, "clientCreateSMTPAuth closing response body")
151
152 if resp.StatusCode != http.StatusCreated {
153 return model.SMTPAuth{}, fmt.Errorf("failed to create smtp auth: %d", resp.StatusCode)
154 }
155
156 var auth model.SMTPAuth
157 if err := json.NewDecoder(resp.Body).Decode(&auth); err != nil {
158 return model.SMTPAuth{}, fmt.Errorf("failed to decode smtp auth response: %w", err)
159 }
160
161 return auth, nil
162}
163
164// clientRemoveSMTPAuth removes an SMTP auth user via the daemon.
165func (c *Command) clientRemoveSMTPAuth(ctx context.Context, name string) error {
166 path, err := url.JoinPath("http://unix/smtp-auth", name)
167 if err != nil {
168 return fmt.Errorf("failed to create remove smtp auth request path: %w", err)
169 }
170 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil)
171 if err != nil {
172 return fmt.Errorf("failed to create delete smtp auth request: %w", err)
173 }
174
175 resp, err := c.httpClient.Do(req)
176 if err != nil {
177 return fmt.Errorf("failed to remove smtp auth: %w", err)
178 }
179 defer model.Wrap(resp.Body.Close, &err, "clientRemoveSMTPAuth closing response body")
180
181 if resp.StatusCode != http.StatusNoContent {
182 return fmt.Errorf("failed to remove smtp auth: %d", resp.StatusCode)
183 }
184
185 return nil
186}
187
188// clientListDKIM fetches the list of DKIM selectors from the daemon.
189func (c *Command) clientListDKIM(ctx context.Context) ([]model.DKIM, error) {
190 resp, err := c.httpClient.Get("http://unix/dkim")
191 if err != nil {
192 return nil, fmt.Errorf("failed to list dkim: %w", err)
193 }
194 defer model.Wrap(resp.Body.Close, &err, "clientListDKIM closing response body")
195
196 if resp.StatusCode != http.StatusOK {
197 return nil, fmt.Errorf("failed to list dkim: %d", resp.StatusCode)
198 }
199
200 var dkims []model.DKIM
201 if err := json.NewDecoder(resp.Body).Decode(&dkims); err != nil {
202 return nil, fmt.Errorf("failed to decode dkim response: %w", err)
203 }
204
205 return dkims, nil
206}
207
208// clientAddDKIM adds a new DKIM selector via the daemon.
209func (c *Command) clientAddDKIM(ctx context.Context, selector, domain string) (string, error) {
210 reqBody := map[string]string{"selector": selector, "domain": domain}
211 body, err := json.Marshal(reqBody)
212 if err != nil {
213 return "", fmt.Errorf("failed to marshal dkim request: %w", err)
214 }
215
216 resp, err := c.httpClient.Post("http://unix/dkim", "application/json", bytes.NewReader(body))
217 if err != nil {
218 return "", fmt.Errorf("failed to add dkim: %w", err)
219 }
220 defer model.Wrap(resp.Body.Close, &err, "clientAddDKIM closing response body")
221
222 if resp.StatusCode != http.StatusOK {
223 return "", fmt.Errorf("failed to add dkim: %d", resp.StatusCode)
224 }
225
226 var result map[string]string
227 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
228 return "", fmt.Errorf("failed to decode dkim response: %w", err)
229 }
230
231 return result["dns_record"], nil
232}
233
234// clientGetDKIM gets the public DNS record for a DKIM selector via the daemon.
235func (c *Command) clientGetDKIM(_ context.Context, selector, domain string) (string, error) {
236 path, err := url.JoinPath("http://unix/dkim", selector, domain)
237 if err != nil {
238 return "", fmt.Errorf("failed to create dkim request path: %w", err)
239 }
240 resp, err := c.httpClient.Get(path)
241 if err != nil {
242 return "", fmt.Errorf("failed to get dkim: %w", err)
243 }
244 defer model.Wrap(resp.Body.Close, &err, "clientGetDKIM closing response body")
245
246 if resp.StatusCode != http.StatusOK {
247 return "", fmt.Errorf("failed to get dkim: %d", resp.StatusCode)
248 }
249
250 var result map[string]string
251 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
252 return "", fmt.Errorf("failed to decode dkim response: %w", err)
253 }
254
255 return result["dns_record"], nil
256}
257
258// clientSetDKIMEnabled enables/disables a DKIM selector via the daemon.
259func (c *Command) clientSetDKIMEnabled(_ context.Context, selector, domain string, enable bool, force bool) error {
260 enableKeyword := "enable"
261 if !enable {
262 enableKeyword = "disable"
263 }
264
265 path, err := url.JoinPath("http://unix/dkim", selector, domain, enableKeyword)
266 if err != nil {
267 return fmt.Errorf("failed to create dkim request path: %w", err)
268 }
269
270 // Include force parameter in query string
271 if !force {
272 path += "?force=false"
273 }
274
275 resp, err := c.httpClient.Post(path, "application/json", nil)
276 if err != nil {
277 return fmt.Errorf("failed to set dkim enabled: %w", err)
278 }
279 defer model.Wrap(resp.Body.Close, &err, "clientSetDKIMEnabled closing response body")
280
281 if resp.StatusCode != http.StatusNoContent {
282 return fmt.Errorf("failed to set dkim enabled: %d", resp.StatusCode)
283 }
284
285 return nil
286}
287
288// clientRemoveDKIM removes a DKIM selector via the daemon.
289func (c *Command) clientRemoveDKIM(ctx context.Context, selector, domain string) error {
290 path, err := url.JoinPath("http://unix/dkim", selector, domain)
291 if err != nil {
292 return fmt.Errorf("failed to create dkim request path: %w", err)
293 }
294 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil)
295 if err != nil {
296 return fmt.Errorf("failed to create delete dkim request: %w", err)
297 }
298
299 resp, err := c.httpClient.Do(req)
300 if err != nil {
301 return fmt.Errorf("failed to remove dkim: %w", err)
302 }
303 defer model.Wrap(resp.Body.Close, &err, "clientRemoveDKIM closing response body")
304
305 if resp.StatusCode != http.StatusNoContent {
306 return fmt.Errorf("failed to remove dkim: %d", resp.StatusCode)
307 }
308
309 return nil
310}
311
312// clientPruneMails prunes orphaned mails via the daemon.
313func (c *Command) clientPruneMails(_ context.Context) (int, error) {
314 resp, err := c.httpClient.Post("http://unix/prune", "application/json", nil)
315 if err != nil {
316 return 0, fmt.Errorf("failed to prune mails: %w", err)
317 }
318 defer model.Wrap(resp.Body.Close, &err, "clientPruneMails closing response body")
319
320 if resp.StatusCode != http.StatusOK {
321 return 0, fmt.Errorf("failed to prune mails: %d", resp.StatusCode)
322 }
323
324 var result map[string]int
325 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
326 return 0, fmt.Errorf("failed to decode prune response: %w", err)
327 }
328
329 return result["pruned"], nil
330}
331
332// clientKVList fetches all key-value pairs from the daemon.
333func (c *Command) clientKVList(_ context.Context) (map[string]any, error) {
334 resp, err := c.httpClient.Get("http://unix/kv")
335 if err != nil {
336 return nil, fmt.Errorf("failed to list kv: %w", err)
337 }
338 defer model.Wrap(resp.Body.Close, &err, "clientKVList closing response body")
339
340 if resp.StatusCode != http.StatusOK {
341 return nil, fmt.Errorf("failed to list kv: %d", resp.StatusCode)
342 }
343
344 var pairs map[string]any
345 if err := json.NewDecoder(resp.Body).Decode(&pairs); err != nil {
346 return nil, fmt.Errorf("failed to decode kv response: %w", err)
347 }
348
349 return pairs, nil
350}
351
352// clientKVGet fetches the value stored under key from the daemon.
353// The key is URL-encoded so keys containing slashes work.
354func (c *Command) clientKVGet(_ context.Context, key string) (any, error) {
355 path := "http://unix/kv/" + url.PathEscape(key)
356
357 resp, err := c.httpClient.Get(path)
358 if err != nil {
359 return nil, fmt.Errorf("failed to get kv: %w", err)
360 }
361 defer model.Wrap(resp.Body.Close, &err, "clientKVGet closing response body")
362
363 if resp.StatusCode != http.StatusOK {
364 return nil, fmt.Errorf("failed to get kv: %d", resp.StatusCode)
365 }
366
367 var result struct {
368 Value any `json:"value"`
369 }
370 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
371 return nil, fmt.Errorf("failed to decode kv response: %w", err)
372 }
373
374 return result.Value, nil
375}
376
377// clientKVPop removes the value stored under key via the daemon and returns it.
378// The key is URL-encoded so keys containing slashes work.
379func (c *Command) clientKVPop(ctx context.Context, key string) (any, error) {
380 path := "http://unix/kv/" + url.PathEscape(key)
381 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil)
382 if err != nil {
383 return nil, fmt.Errorf("failed to create pop kv request: %w", err)
384 }
385
386 resp, err := c.httpClient.Do(req)
387 if err != nil {
388 return nil, fmt.Errorf("failed to pop kv: %w", err)
389 }
390 defer model.Wrap(resp.Body.Close, &err, "clientKVPop closing response body")
391
392 if resp.StatusCode != http.StatusOK {
393 return nil, fmt.Errorf("failed to pop kv: %d", resp.StatusCode)
394 }
395
396 var result struct {
397 Value any `json:"value"`
398 }
399 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
400 return nil, fmt.Errorf("failed to decode kv response: %w", err)
401 }
402
403 return result.Value, nil
404}