Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions forwardemail/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{
Expand All @@ -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",
},
}
Expand Down
49 changes: 49 additions & 0 deletions forwardemail/encrypt.go
Original file line number Diff line number Diff line change
@@ -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
}
66 changes: 66 additions & 0 deletions forwardemail/encrypt_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
10 changes: 10 additions & 0 deletions forwardemail/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions tools/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading