dns.go raw
1 package mailinabox
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/url"
10 "strings"
11 )
12
13 // Record Represents a DNS record.
14 type Record struct {
15 Name string `json:"qname,omitempty"`
16 Type string `json:"rtype,omitempty"`
17 Value string `json:"value,omitempty"`
18 Explanation string `json:"explanation,omitempty"`
19 }
20
21 // Zone Represents a DNS zone.
22 type Zone struct {
23 Zone string
24 Records []Record
25 }
26
27 // Zones a slice of Zones.
28 // Use a custom unmarshalling method.
29 type Zones []Zone
30
31 // UnmarshalJSON customs unmarshalling.
32 func (z *Zones) UnmarshalJSON(data []byte) error {
33 if string(data) == "null" || string(data) == `""` {
34 return nil
35 }
36
37 var a []json.RawMessage
38 if err := json.Unmarshal(data, &a); err != nil {
39 return err
40 }
41
42 var all []Zone
43
44 for _, message := range a {
45 var b []json.RawMessage
46 if err := json.Unmarshal(message, &b); err != nil {
47 return err
48 }
49
50 zone := Zone{}
51
52 if err := json.Unmarshal(b[0], &zone.Zone); err != nil {
53 return err
54 }
55
56 if len(b) <= 1 {
57 all = append(all, zone)
58 continue
59 }
60
61 if err := json.Unmarshal(b[1], &zone.Records); err != nil {
62 return err
63 }
64
65 all = append(all, zone)
66 }
67
68 *z = all
69
70 return nil
71 }
72
73 // Nameserver Represents DNS nameservers.
74 type Nameserver struct {
75 Hostnames []string `json:"hostnames"`
76 }
77
78 // DNSService DNS API.
79 // https://mailinabox.email/api-docs.html#tag/DNS
80 type DNSService service
81
82 // GetSecondaryNameserver Returns a list of nameserver hostnames.
83 // https://mailinabox.email/api-docs.html#operation/getDnsSecondaryNameserver
84 func (s *DNSService) GetSecondaryNameserver(ctx context.Context) ([]string, error) {
85 endpoint := s.client.baseURL.JoinPath("admin", "dns", "secondary-nameserver")
86
87 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
88 if err != nil {
89 return nil, fmt.Errorf("unable to create request: %w", err)
90 }
91
92 var results Nameserver
93
94 err = s.client.doJSON(req, &results)
95 if err != nil {
96 return nil, err
97 }
98
99 return results.Hostnames, nil
100 }
101
102 // AddSecondaryNameserver Adds one or more secondary nameservers.
103 // https://mailinabox.email/api-docs.html#operation/addDnsSecondaryNameserver
104 func (s *DNSService) AddSecondaryNameserver(ctx context.Context, hostnames []string) (string, error) {
105 endpoint := s.client.baseURL.JoinPath("admin", "dns", "secondary-nameserver")
106
107 data := url.Values{}
108 data.Set("hostnames", strings.Join(hostnames, ","))
109
110 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(data.Encode()))
111 if err != nil {
112 return "", fmt.Errorf("unable to create request: %w", err)
113 }
114
115 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
116
117 resp, err := s.client.doPlain(req)
118 if err != nil {
119 return "", err
120 }
121
122 return strings.TrimSpace(string(resp)), nil
123 }
124
125 // GetZones Returns an array of all managed top-level domains.
126 // https://mailinabox.email/api-docs.html#operation/getDnsZones
127 func (s *DNSService) GetZones(ctx context.Context) ([]string, error) {
128 endpoint := s.client.baseURL.JoinPath("admin", "dns", "zones")
129
130 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
131 if err != nil {
132 return nil, fmt.Errorf("unable to create request: %w", err)
133 }
134
135 var results []string
136
137 err = s.client.doJSON(req, &results)
138 if err != nil {
139 return nil, err
140 }
141
142 return results, nil
143 }
144
145 // GetZoneFile Returns a DNS zone file for a hostname.
146 // https://mailinabox.email/api-docs.html#operation/getDnsZonefile
147 func (s *DNSService) GetZoneFile(ctx context.Context, zone string) (string, error) {
148 endpoint := s.client.baseURL.JoinPath("admin", "dns", "zonefile", zone)
149
150 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
151 if err != nil {
152 return "", fmt.Errorf("unable to create request: %w", err)
153 }
154
155 var results string
156
157 err = s.client.doJSON(req, &results)
158 if err != nil {
159 return "", err
160 }
161
162 return results, nil
163 }
164
165 // UpdateDNS Updates the DNS. Involves creating zone files and restarting `nsd`.
166 // https://mailinabox.email/api-docs.html#operation/updateDns
167 func (s *DNSService) UpdateDNS(ctx context.Context, force bool) (string, error) {
168 endpoint := s.client.baseURL.JoinPath("admin", "dns", "update")
169
170 data := url.Values{}
171 data.Set("force", boolToIntStr(force))
172
173 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(data.Encode()))
174 if err != nil {
175 return "", fmt.Errorf("unable to create request: %w", err)
176 }
177
178 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
179
180 resp, err := s.client.doPlain(req)
181 if err != nil {
182 return "", err
183 }
184
185 return strings.TrimSpace(string(resp)), nil
186 }
187
188 // GetAllRecords Returns all custom DNS records.
189 // https://mailinabox.email/api-docs.html#operation/getDnsCustomRecords
190 func (s *DNSService) GetAllRecords(ctx context.Context) ([]Record, error) {
191 endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom")
192
193 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
194 if err != nil {
195 return nil, fmt.Errorf("unable to create request: %w", err)
196 }
197
198 var results []Record
199
200 err = s.client.doJSON(req, &results)
201 if err != nil {
202 return nil, err
203 }
204
205 return results, nil
206 }
207
208 // GetRecords Returns all custom records for the specified query name and type.
209 // https://mailinabox.email/api-docs.html#operation/getDnsCustomRecordsForQNameAndType
210 func (s *DNSService) GetRecords(ctx context.Context, name, rType string) ([]Record, error) {
211 if name == "" || rType == "" {
212 return nil, errors.New("qname and rtype are required")
213 }
214
215 endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name, rType)
216
217 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
218 if err != nil {
219 return nil, fmt.Errorf("unable to create request: %w", err)
220 }
221
222 var results []Record
223
224 err = s.client.doJSON(req, &results)
225 if err != nil {
226 return nil, err
227 }
228
229 return results, nil
230 }
231
232 // AddRecord Adds a custom DNS record for the specified query name and type.
233 // https://mailinabox.email/api-docs.html#operation/addDnsCustomRecord
234 func (s *DNSService) AddRecord(ctx context.Context, record Record) (string, error) {
235 if record.Name == "" || record.Type == "" {
236 return "", errors.New("qname and rtype are required")
237 }
238
239 endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", record.Name, record.Type)
240
241 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(record.Value))
242 if err != nil {
243 return "", fmt.Errorf("unable to create request: %w", err)
244 }
245
246 resp, err := s.client.doPlain(req)
247 if err != nil {
248 return "", err
249 }
250
251 return strings.TrimSpace(string(resp)), nil
252 }
253
254 // UpdateRecord Updates an existing DNS custom record value for the specified qname and type.
255 // https://mailinabox.email/api-docs.html#operation/updateDnsCustomRecord
256 func (s *DNSService) UpdateRecord(ctx context.Context, record Record, value string) (string, error) {
257 if record.Name == "" || record.Type == "" {
258 return "", errors.New("qname and rtype are required")
259 }
260
261 endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", record.Name, record.Type)
262
263 req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint.String(), strings.NewReader(value))
264 if err != nil {
265 return "", fmt.Errorf("unable to create request: %w", err)
266 }
267
268 resp, err := s.client.doPlain(req)
269 if err != nil {
270 return "", err
271 }
272
273 return strings.TrimSpace(string(resp)), nil
274 }
275
276 // RemoveRecord Removes a DNS custom record for the specified domain, type & value.
277 // https://mailinabox.email/api-docs.html#operation/removeDnsCustomRecord
278 func (s *DNSService) RemoveRecord(ctx context.Context, record Record) (string, error) {
279 if record.Name == "" || record.Type == "" {
280 return "", errors.New("qname and rtype are required")
281 }
282
283 endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", record.Name, record.Type)
284
285 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint.String(), strings.NewReader(record.Value))
286 if err != nil {
287 return "", fmt.Errorf("unable to create request: %w", err)
288 }
289
290 resp, err := s.client.doPlain(req)
291 if err != nil {
292 return "", err
293 }
294
295 return strings.TrimSpace(string(resp)), nil
296 }
297
298 // GetARecords Returns all custom A records for the specified query name.
299 // https://mailinabox.email/api-docs.html#operation/getDnsCustomARecordsForQName
300 func (s *DNSService) GetARecords(ctx context.Context, name string) ([]Record, error) {
301 if name == "" {
302 return nil, errors.New("qname is required")
303 }
304
305 endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name)
306
307 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
308 if err != nil {
309 return nil, fmt.Errorf("unable to create request: %w", err)
310 }
311
312 var results []Record
313
314 err = s.client.doJSON(req, &results)
315 if err != nil {
316 return nil, err
317 }
318
319 return results, nil
320 }
321
322 // AddARecord Adds a custom DNS A record for the specified query name.
323 // https://mailinabox.email/api-docs.html#operation/addDnsCustomARecord
324 func (s *DNSService) AddARecord(ctx context.Context, name, value string) (string, error) {
325 if name == "" {
326 return "", errors.New("qname is required")
327 }
328
329 endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name)
330
331 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(value))
332 if err != nil {
333 return "", fmt.Errorf("unable to create request: %w", err)
334 }
335
336 resp, err := s.client.doPlain(req)
337 if err != nil {
338 return "", err
339 }
340
341 return strings.TrimSpace(string(resp)), nil
342 }
343
344 // UpdateARecord Updates an existing DNS custom A record value for the specified qname.
345 // https://mailinabox.email/api-docs.html#operation/updateDnsCustomARecord
346 func (s *DNSService) UpdateARecord(ctx context.Context, name, value string) (string, error) {
347 if name == "" {
348 return "", errors.New("qname is required")
349 }
350
351 endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name)
352
353 req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint.String(), strings.NewReader(value))
354 if err != nil {
355 return "", fmt.Errorf("unable to create request: %w", err)
356 }
357
358 resp, err := s.client.doPlain(req)
359 if err != nil {
360 return "", err
361 }
362
363 return strings.TrimSpace(string(resp)), nil
364 }
365
366 // RemoveARecord Removes a DNS custom A record for the specified domain & value.
367 // https://mailinabox.email/api-docs.html#operation/removeDnsCustomARecord
368 func (s *DNSService) RemoveARecord(ctx context.Context, name, value string) (string, error) {
369 if name == "" {
370 return "", errors.New("qname is required")
371 }
372
373 endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name)
374
375 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint.String(), strings.NewReader(value))
376 if err != nil {
377 return "", fmt.Errorf("unable to create request: %w", err)
378 }
379
380 resp, err := s.client.doPlain(req)
381 if err != nil {
382 return "", err
383 }
384
385 return strings.TrimSpace(string(resp)), nil
386 }
387
388 // GetDump Returns all DNS records.
389 // https://mailinabox.email/api-docs.html#operation/getDnsDump
390 func (s *DNSService) GetDump(ctx context.Context) ([]Zone, error) {
391 endpoint := s.client.baseURL.JoinPath("admin", "dns", "dump")
392
393 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
394 if err != nil {
395 return nil, fmt.Errorf("unable to create request: %w", err)
396 }
397
398 var results Zones
399
400 err = s.client.doJSON(req, &results)
401 if err != nil {
402 return nil, err
403 }
404
405 return results, nil
406 }
407