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