all repos — postern

@ 9799a3215ac0185fb9b4e7d6ec0645c2cb2ce55b

Modern mail management

clone
git clone https:///postern.git

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, 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 resp.Body.Close()
 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, 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 resp.Body.Close()
 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) 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 resp.Body.Close()
 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 resp.Body.Close()
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 resp.Body.Close()
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(ctx 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 resp.Body.Close()
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 resp.Body.Close()
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 resp.Body.Close()
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 resp.Body.Close()
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(ctx 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 resp.Body.Close()
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(ctx 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 resp.Body.Close()
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 resp.Body.Close()
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(ctx 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 resp.Body.Close()
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}