Files
mailinabox-go/dns.go
T

131 lines
3.7 KiB
Go

package miab
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// Client holds connection configuration for the Mail-in-a-Box admin DNS API.
type Client struct {
host string
username string
password string
httpClient *http.Client
}
// DNSRecord represents a single custom DNS record returned by the MIAB API.
type DNSRecord struct {
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
}
// NewClient constructs a Client. host should be the bare hostname (e.g. "box.example.com").
func NewClient(host, username, password string) *Client {
return &Client{
host: host,
username: username,
password: password,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// SetRecord replaces all existing records of the given type for name with value (HTTP PUT).
func (c *Client) SetRecord(name, recordType, value string) error {
_, err := c.do(http.MethodPut, c.buildURL(name, recordType), value)
return err
}
// DeleteRecord removes the record matching name, recordType and value (HTTP DELETE).
// Pass an empty value to remove all records of that type for the name.
func (c *Client) DeleteRecord(name, recordType, value string) error {
_, err := c.do(http.MethodDelete, c.buildURL(name, recordType), value)
return err
}
// AddRecord adds a record without replacing existing ones (HTTP POST).
func (c *Client) AddRecord(name, recordType, value string) error {
_, err := c.do(http.MethodPost, c.buildURL(name, recordType), value)
return err
}
// ListRecords returns all custom DNS records. If recordType is non-empty, only
// records of that type are returned.
func (c *Client) ListRecords(recordType string) ([]DNSRecord, error) {
body, err := c.do(http.MethodGet, fmt.Sprintf("https://%s/admin/dns/custom", c.host), "")
if err != nil {
return nil, err
}
// MIAB returns an array of objects with fields: qname, rtype, value
var raw []struct {
QName string `json:"qname"`
RType string `json:"rtype"`
Value string `json:"value"`
}
if err := json.Unmarshal([]byte(body), &raw); err != nil {
return nil, fmt.Errorf("miab: failed to parse response: %w", err)
}
var records []DNSRecord
upper := strings.ToUpper(recordType)
for _, r := range raw {
if recordType == "" || r.RType == upper {
records = append(records, DNSRecord{
Name: r.QName,
Type: r.RType,
Value: r.Value,
})
}
}
return records, nil
}
// buildURL mirrors miab.py's get_miab_url: omits the record type segment for "A" records.
func (c *Client) buildURL(name, recordType string) string {
if strings.ToUpper(recordType) == "A" {
return fmt.Sprintf("https://%s/admin/dns/custom/%s", c.host, name)
}
return fmt.Sprintf("https://%s/admin/dns/custom/%s/%s", c.host, name, strings.ToUpper(recordType))
}
// do executes an HTTP request with Basic Auth and returns the response body as a string.
func (c *Client) do(method, url, body string) (string, error) {
var bodyReader io.Reader
if body != "" {
bodyReader = strings.NewReader(body)
}
req, err := http.NewRequest(method, url, bodyReader)
if err != nil {
return "", fmt.Errorf("miab: failed to create request: %w", err)
}
req.SetBasicAuth(c.username, c.password)
if body != "" {
req.Header.Set("Content-Type", "text/plain")
}
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("miab: request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("miab: failed to read response body: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("miab: unexpected status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
}
return string(respBody), nil
}