package sms // ⚠️ VERIFY BEFORE REAL SMS (scan-flow task 9): the exact canonical-string // format for the HMAC signature and the JSON field names must be confirmed // against the account holder's MXT docs / their existing integration before // the real-SMS test. The Sender interface keeps this implementation swappable // if the auth scheme differs. import ( "bytes" "crypto/hmac" "crypto/md5" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "time" ) // Client sends SMS via the SMSGlobal REST API. type Client struct { apiKey string apiSecret string from string // empty => SMSGlobal shared/pooled number http *http.Client } // New returns an SMSGlobal client. from may be empty to use the shared pool. func New(apiKey, apiSecret, from string) *Client { return &Client{ apiKey: apiKey, apiSecret: apiSecret, from: from, http: &http.Client{Timeout: 10 * time.Second}, } } // Send posts a message to the given destination (international format). func (c *Client) Send(to, body string) error { payload, err := json.Marshal(map[string]string{ "message": body, "to": to, "from": c.from, }) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, "https://api.smsglobal.com/sms/", bytes.NewReader(payload)) if err != nil { return err } date := time.Now().UTC().Format(http.TimeFormat) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.Header.Set("Date", date) req.Header.Set("Authorization", c.authorization(req, payload, date)) resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 300 { b, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) return fmt.Errorf("smsglobal: status %d: %s", resp.StatusCode, string(b)) } return nil } // authorization builds the REST v1 HMAC-SHA256 signature: // base64(HMAC-SHA256(canonicalString, apiSecret)), sent as "key:signature". // Canonical string per SMSGlobal docs: method&path&accept&date&contentType&contentMd5. func (c *Client) authorization(req *http.Request, body []byte, date string) string { bodyHash := md5.Sum(body) canonical := fmt.Sprintf("%s&%s&%s&%s&%s&%s", req.Method, req.URL.Path, "application/json", date, "application/json", base64.StdEncoding.EncodeToString(bodyHash[:])) mac := hmac.New(sha256.New, []byte(c.apiSecret)) mac.Write([]byte(canonical)) sig := base64.StdEncoding.EncodeToString(mac.Sum(nil)) return fmt.Sprintf("%s:%s", c.apiKey, sig) }