all repos — postern @ fc287763b9ce6b6f9823e11c0b1173da0446ebc5

Modern mail management

internal/dkim/dkim.go (view raw)

  1package dkim
  2
  3import (
  4	"bytes"
  5	"context"
  6	"database/sql"
  7	"errors"
  8	"fmt"
  9	"strings"
 10
 11	"postern/internal/db"
 12	"postern/internal/model"
 13	"postern/internal/persistence"
 14)
 15
 16type Config struct {
 17	DB          *db.DB
 18	Persistence *persistence.Persistence
 19}
 20
 21type DKIM struct {
 22	db          *db.DB
 23	persistence *persistence.Persistence
 24}
 25
 26func New(config *Config) *DKIM {
 27	return &DKIM{
 28		db:          config.DB,
 29		persistence: config.Persistence,
 30	}
 31}
 32
 33// Add generates a new RSA-2048 private key for the given selector/domain, stores it
 34// as a blob, inserts the corresponding row in the database and returns the expected
 35// DNS TXT record that should be published before enabling the selector.
 36func (d *DKIM) Add(ctx context.Context, selector, domain string) (string, error) {
 37	if selector == "" {
 38		return "", fmt.Errorf("invalid selector")
 39	}
 40	if strings.Contains(selector, ".") {
 41		return "", fmt.Errorf("selector must not contain '.'")
 42	}
 43	if domain == "" {
 44		return "", fmt.Errorf("invalid domain")
 45	}
 46
 47	privKey, keyType := genRSAPrivateKey()
 48	encodedPrivKey := encodePrivateKey(privKey)
 49
 50	blobName, _, err := d.persistence.WriteBlob(bytes.NewReader(encodedPrivKey))
 51	if err != nil {
 52		return "", err
 53	}
 54
 55	err = d.db.InsertDKIMForDomain(ctx, model.DKIM{
 56		Domain:      domain,
 57		Selector:    selector,
 58		BlobAddress: blobName,
 59		KeyType:     keyType,
 60		Enabled:     false,
 61	})
 62	if err != nil {
 63		return "", err
 64	}
 65
 66	return pubKeyRecord(privKey.Public()), nil
 67}
 68
 69// List returns all configured DKIM selectors.
 70func (d *DKIM) List(ctx context.Context) ([]model.DKIM, error) {
 71	return d.db.ListDKIM(ctx)
 72}
 73
 74// PublicDNSRecord returns the expected DNS TXT record for the given selector/domain
 75// as it should be published under selector._domainkey.domain.
 76func (d *DKIM) PublicDNSRecord(ctx context.Context, selector, domain string) (string, error) {
 77	dbRecord, err := d.db.GetDKIMForSelectorDomain(ctx, selector, domain)
 78	if err != nil {
 79		return "", err
 80	}
 81	privKey, err := d.readPrivateKey(dbRecord.BlobAddress)
 82	if err != nil {
 83		return "", err
 84	}
 85	return pubKeyRecord(privKey.Public()), nil
 86}
 87
 88// SetEnabled enables or disables the given selector/domain selector.
 89func (d *DKIM) SetEnabled(ctx context.Context, selector, domain string, enabled bool) error {
 90	return d.db.SetDKIMEnabled(ctx, selector, domain, enabled)
 91}
 92
 93// Remove deletes the private key and the database entry.
 94func (d *DKIM) Remove(ctx context.Context, selector, domain string) error {
 95	dkimRow, err := d.db.GetDKIMForSelectorDomain(ctx, selector, domain)
 96	if err != nil {
 97		if errors.Is(err, sql.ErrNoRows) {
 98			return errors.New("DKIM entry not found")
 99		}
100		return err
101	}
102
103	tx, err := d.db.GetWriteTx(ctx)
104	if err != nil {
105		return err
106	}
107
108	err = db.RemoveDKIM(ctx, tx, selector, domain)
109	if err != nil {
110		_ = tx.Rollback()
111		return err
112	}
113
114	err = d.persistence.RemoveBlob(dkimRow.BlobAddress)
115	if err != nil {
116		_ = tx.Rollback()
117		return err
118	}
119
120	return tx.Commit()
121}