mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-13 08:02:34 +00:00
feat: deny retired and unassigned mailbox warmup actions without retrying authoritative absence or weakening send safety
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI
parent
c85b2bcb23
commit
4e1c2e6053
@@ -636,6 +636,8 @@ Cloud-managed OAuth additionally exposes `POST /pool-link/instance/oauth/start-c
|
||||
|
||||
`POST /internal/worker/outbound-dispatch`, `/internal/worker/warmup-actions`, and `/internal/worker/diagnostic-auth` use the existing execution-plane internal token, never customer API keys. They bind the current mailbox/worker and require protocol 2. Outbound start additionally rechecks task/organization/provider/nonce/recipients and current campaign or diagnostic policy. Diagnostic authorization/result submission binds one exact already-owned warmup receipt and short-lived nonce; it neither ingests unrelated inbox messages nor trusts event/header claims. No public scope or permission was added. Details and limits: [send safety](/development/send-safety/).
|
||||
|
||||
Warmup actions for a deleted, unassigned or no-longer-authorized mailbox return no permitted actions. The worker treats that authoritative denial as a no-op without touching the provider. Database or authority failures still return `503` and retry; denial does not authorize a send, clear a send hold, or acknowledge an arrival.
|
||||
|
||||
### Referrals and discounts
|
||||
|
||||
The referral program and billing discount history are part of `/subscription/*`, so they are JWT only and never accept an API key. There is no API permission scope for them; browser callers are gated by the org permission below. The discount validation and checkout endpoints are listed under [Subscription and billing](/api/reference/account-org/) in the reference.
|
||||
|
||||
@@ -9,8 +9,36 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/warmbly/warmbly/internal/config"
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
"github.com/warmbly/warmbly/internal/repository"
|
||||
)
|
||||
|
||||
type warmupActionAuthorityStub struct {
|
||||
repository.SyncContextRepository
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *warmupActionAuthorityStub) PermittedWarmupActions(context.Context, uuid.UUID, uuid.UUID, []string) ([]string, error) {
|
||||
return nil, s.err
|
||||
}
|
||||
|
||||
func TestWarmupActionAuthorityDenialDoesNotTouchMailbox(t *testing.T) {
|
||||
w := &WorkerService{ID: uuid.NewString(), SyncContextRepository: &warmupActionAuthorityStub{}}
|
||||
for _, action := range []string{models.WarmupActionFile, models.WarmupActionDelete, models.WarmupActionVerifyRemoval} {
|
||||
if err := w.HandleWarmupAction(t.Context(), models.WarmupEmailAction{EmailID: uuid.New(), Actions: []string{action}, FilingID: uuid.NewString()}); err != nil {
|
||||
t.Fatalf("denied %s action must finish without loading a mailbox: %v", action, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarmupActionAuthorityFailureMustRetry(t *testing.T) {
|
||||
failure := errors.New("authority unavailable")
|
||||
w := &WorkerService{ID: uuid.NewString(), SyncContextRepository: &warmupActionAuthorityStub{err: failure}}
|
||||
err := w.HandleWarmupAction(t.Context(), models.WarmupEmailAction{EmailID: uuid.New(), Actions: []string{models.WarmupActionDelete}})
|
||||
if !errors.Is(err, failure) {
|
||||
t.Fatalf("authority failure must remain retryable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarmupFilingFallsBackForOlderEvents(t *testing.T) {
|
||||
// An event published before the placement fields existed has to behave the
|
||||
// way every mailbox did then: the default folder.
|
||||
|
||||
@@ -2,12 +2,49 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
)
|
||||
|
||||
func TestLiveWarmupActionsDenyUnavailableMailbox(t *testing.T) {
|
||||
for _, state := range []string{"missing", "deleted", "unassigned"} {
|
||||
t.Run(state, func(t *testing.T) {
|
||||
f, r := lineageFixture(t)
|
||||
worker := uuid.New()
|
||||
mailbox := f.recipient
|
||||
switch state {
|
||||
case "missing":
|
||||
mailbox = uuid.New()
|
||||
case "deleted":
|
||||
if _, err := f.pool.Exec(t.Context(), `DELETE FROM email_accounts WHERE id=$1`, mailbox); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case "unassigned":
|
||||
if _, err := f.pool.Exec(t.Context(), `UPDATE email_accounts SET worker_id=NULL WHERE id=$1`, mailbox); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
actions, err := r.PermittedWarmupActions(t.Context(), mailbox, worker, []string{models.WarmupActionFile, models.WarmupActionDelete})
|
||||
if err != nil || len(actions) != 0 {
|
||||
t.Fatalf("unavailable mailbox must deny provider actions without retry: actions=%v err=%v", actions, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveWarmupActionsPreserveAuthorityFailure(t *testing.T) {
|
||||
f, r := lineageFixture(t)
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
actions, err := r.PermittedWarmupActions(ctx, f.recipient, uuid.New(), []string{models.WarmupActionDelete})
|
||||
if !errors.Is(err, context.Canceled) || len(actions) != 0 {
|
||||
t.Fatalf("authority failure must remain retryable: actions=%v err=%v", actions, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveDiagnosticOffStopsQueuedSendAndActions(t *testing.T) {
|
||||
f, r := lineageFixture(t)
|
||||
ctx := t.Context()
|
||||
@@ -64,6 +101,10 @@ func TestLiveDiagnosticOffStopsQueuedSendAndActions(t *testing.T) {
|
||||
if err != nil || len(got) != len(actions) {
|
||||
t.Fatal("explicit legacy behavior changed", got, err)
|
||||
}
|
||||
got, err = r.PermittedWarmupActions(ctx, f.recipient, uuid.New(), actions)
|
||||
if err != nil || len(got) != 0 {
|
||||
t.Fatal("a different worker gained provider action authority", got, err)
|
||||
}
|
||||
exec(`UPDATE email_accounts SET test_mode='off' WHERE id=$1`, f.recipient)
|
||||
got, err = r.PermittedWarmupActions(ctx, f.recipient, worker, actions)
|
||||
if err != nil || len(got) != 1 || got[0] != models.WarmupActionDelete {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
)
|
||||
|
||||
@@ -24,6 +25,9 @@ func (r *taskRepository) PermittedWarmupActions(ctx context.Context, mailbox, wo
|
||||
err := r.db.QueryRow(ctx, `SELECT ea.status='active' AND ea.worker_id=$2 AND n.role='worker' AND n.active AND n.last_seen_at>NOW()-INTERVAL '10 minutes' AND n.warmup_send_protocol>=2
|
||||
AND o.risk_state IN ('trusted','watch') AND NOT EXISTS(SELECT 1 FROM cloud_link_mailboxes c WHERE c.email_account_id=ea.id),ea.test_mode,ea.test_send_enabled,ea.test_receive_enabled
|
||||
FROM email_accounts ea JOIN organizations o ON o.id=ea.organization_id JOIN fleet_nodes n ON n.id=ea.worker_id WHERE ea.id=$1`, mailbox, worker).Scan(&active, &a.TestMode, &a.TestSendEnabled, &a.TestReceiveEnabled)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user