diff --git a/forwardemail/client_test.go b/forwardemail/client_test.go index abe96b3..c31b6be 100644 --- a/forwardemail/client_test.go +++ b/forwardemail/client_test.go @@ -29,9 +29,9 @@ func TestNewClient(t *testing.T) { wantURL: "https://api.forwardemail.net", }, { - name: "with custom URL", - apiKey: "4e4d6c332b6fe62a63afe56171fd3725", - opts: []Option{WithAPIURL("https://google.com")}, + name: "with custom URL", + apiKey: "4e4d6c332b6fe62a63afe56171fd3725", + opts: []Option{WithAPIURL("https://google.com")}, wantURL: "https://google.com", }, { @@ -41,9 +41,9 @@ func TestNewClient(t *testing.T) { wantError: ErrNilHTTPClient, }, { - name: "with custom http client", - apiKey: "4e4d6c332b6fe62a63afe56171fd3725", - opts: []Option{WithHTTPClient(&http.Client{Timeout: 10 * time.Second})}, + name: "with custom http client", + apiKey: "4e4d6c332b6fe62a63afe56171fd3725", + opts: []Option{WithHTTPClient(&http.Client{Timeout: 10 * time.Second})}, wantURL: "https://api.forwardemail.net", }, } diff --git a/forwardemail/encrypt.go b/forwardemail/encrypt.go new file mode 100644 index 0000000..e1d5850 --- /dev/null +++ b/forwardemail/encrypt.go @@ -0,0 +1,49 @@ +// Copyright Forward Email 2026 +// SPDX-License-Identifier: MIT + +package forwardemail + +// Forward Email allows you to encrypt records even on the free plan at no cost. As highly requested in a Privacy Guides discussion and on our GitHub issues we've added this. + +import ( + "context" + "encoding/json" + "strings" +) + +// EncryptionResponse represents the API Response for encrypting a TXT record. +type EncryptionResponse struct { + Encrypted string `json:"encrypted"` +} + +// EncryptRecord allows you to encrypt a TXT record value so that you can safely add it to your DNS without exposing the value in plaintext. +func (c *Client) EncryptRecord(ctx context.Context, input string) (*EncryptionResponse, error) { + if ctx == nil { + return nil, ErrNilContext + } + if err := ctx.Err(); err != nil { + return nil, err + } + + if input == "" { + return nil, ErrMissingEncryptionInput + } + + payload := strings.NewReader(`{"input": "` + input + `"}`) + req, err := c.newRequest(ctx, "POST", "/v1/encrypt", payload) + if err != nil { + return nil, ErrFailedToCreateRequest + } + + res, err := c.doRequest(req) + if err != nil { + return nil, ErrFailedToDoRequest + } + + var encryptionResponse EncryptionResponse + if err := json.Unmarshal(res, &encryptionResponse); err != nil { + return nil, ErrFailedToUnmarshalResponse + } + + return &encryptionResponse, nil +} diff --git a/forwardemail/encrypt_test.go b/forwardemail/encrypt_test.go new file mode 100644 index 0000000..c6d0f50 --- /dev/null +++ b/forwardemail/encrypt_test.go @@ -0,0 +1,66 @@ +// Copyright Forward Email 2026 +// SPDX-License-Identifier: MIT + +package forwardemail + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestClient_EncryptRecord(t *testing.T) { + const input = "forward-email-site-verification=abc123" + + response := `{ + "encrypted": "forward-email-site-verification=$2a$10$abc123" + }` + want := &EncryptionResponse{ + Encrypted: "forward-email-site-verification=$2a$10$abc123", + } + + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST method, got %s", r.Method) + } + if r.URL.Path != "/v1/encrypt" { + t.Errorf("expected URL /v1/encrypt, got %s", r.URL.Path) + } + + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("failed to read request body: %v", err) + } + if string(body) != fmt.Sprintf(`{"input": "%s"}`, input) { + t.Errorf("unexpected request body %q", string(body)) + } + + fmt.Fprint(w, response) + })) + defer svr.Close() + + c, _ := NewClient("test-key", WithAPIURL(svr.URL)) + + got, err := c.EncryptRecord(context.Background(), input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("values are not the same %s", diff) + } +} + +func TestClient_EncryptRecordMissingInput(t *testing.T) { + c, _ := NewClient("test-key") + + _, err := c.EncryptRecord(context.Background(), "") + if !errors.Is(err, ErrMissingEncryptionInput) { + t.Fatalf("expected error %v, got %v", ErrMissingEncryptionInput, err) + } +} diff --git a/forwardemail/errors.go b/forwardemail/errors.go index 538d12b..84a2bfe 100644 --- a/forwardemail/errors.go +++ b/forwardemail/errors.go @@ -22,6 +22,13 @@ var ( ErrEmptyPath = errors.New("request path cannot be empty") // ErrNilHTTPClient is returned when the client has a nil HTTPClient. ErrNilHTTPClient = errors.New("HTTP client cannot be nil") + // ErrFailedToCreateRequest is returned when a new HTTP request cannot be created. + ErrFailedToCreateRequest = errors.New("failed to create HTTP request") + // ErrFailedToDoRequest is returned when an HTTP request cannot be completed. + ErrFailedToDoRequest = errors.New("failed to complete HTTP request") + // ErrFailedToUnmarshalResponse is returned when an API response cannot be unmarshaled. + ErrFailedToUnmarshalResponse = errors.New("failed to unmarshal API response") + // ErrEmptyDomain is returned when a domain parameter is empty. ErrEmptyDomain = errors.New("domain cannot be empty") // ErrEmptyAlias is returned when an alias parameter is empty. @@ -32,6 +39,9 @@ var ( ErrEmptyEmail = errors.New("email cannot be empty") // ErrEmptyGroup is returned when a group parameter is empty. ErrEmptyGroup = errors.New("group cannot be empty") + + // ErrMissingEncryptionInput is returned when required input for encryption is missing. + ErrMissingEncryptionInput = errors.New("missing required input for encryption") ) // APIError represents an error response from the Forward Email API. diff --git a/tools/tools.go b/tools/tools.go index d0b8660..047ba51 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -3,10 +3,10 @@ package tools import ( + _ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint" _ "github.com/hashicorp/copywrite" - _ "mvdan.cc/gofumpt" _ "golang.org/x/tools/cmd/goimports" - _ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint" + _ "mvdan.cc/gofumpt" ) // Generate copyright headers