feat: align AWS migration setup with current self-hosted mailbox organization handling

This commit is contained in:
Matthew Meszaros
2026-10-22 09:01:37 +02:00
3 changed files with 43 additions and 3 deletions
+1 -1
View File
@@ -165,7 +165,7 @@ Pause and resume per mailbox from the same row. Once a mailbox is enrolled, its
Warmbly Cloud owns an enrolled mailbox's starting volume, daily increase, maximum, reply rate, sending window and sending days. Your instance keeps a copy of them on the mailbox, so the Warmup tab, the API and campaign estimates show what Warmbly Cloud actually runs.
- The first enrollment proposes the settings the mailbox has on your instance. Enrolling it again, or refreshing its credential after a password change, never sends them again, so neither can undo a change made in Warmbly Cloud.
- Changing a setting on the Warmup tab, or through `PATCH /emails/:id`, sends it to Warmbly Cloud first. It is saved only once Warmbly Cloud has applied it, and the mailbox then shows the values Warmbly Cloud reports. If Warmbly Cloud cannot be reached, nothing changes. If it answers but does not apply a value, the rest of your change is not saved, the error names what was not applied, and the mailbox shows the values Warmbly Cloud reported.
- Changing a setting on the Warmup tab, or through `PATCH /emails/:id`, sends it to Warmbly Cloud first. You can change the starting volume and other ramp settings while warmup is active without removing or enrolling the mailbox again. It is saved only once Warmbly Cloud has applied it, and the mailbox then shows the values Warmbly Cloud reports. If Warmbly Cloud cannot be reached, nothing changes. If it answers but does not apply a value, the rest of your change is not saved, the error names what was not applied, and the mailbox shows the values Warmbly Cloud reported.
- The copy is refreshed whenever the Cloud mailbox list loads, after every change, and every 15 minutes in the background, so a change made in Warmbly Cloud shows up here within 15 minutes. The Warmup tab shows when it was last refreshed.
- The timezone stays your instance's, because campaigns use it too. It travels with every change, and the background refresh sends it again whenever Warmbly Cloud's differs.
- Warmbly Cloud does not take 0 from an instance for the starting volume, maximum, daily increase or reply rate, so on an enrolled mailbox those fields start at 1. Sending every day is sent as all seven days.
@@ -0,0 +1,40 @@
package repository
import (
"testing"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
)
func TestLiveMailboxGetCarriesOrganizationForWarmupEdits(t *testing.T) {
handle, pool := liveContactDB(t)
f := newWarmupUsageFixture(t, pool)
r := NewEmailRepostory(handle, nil)
account, xerr := r.Get(t.Context(), f.org.String(), f.account.String())
if xerr != nil {
t.Fatal(xerr)
}
if account.OrganizationID == nil || *account.OrganizationID != f.org {
t.Fatalf("mailbox organization = %v, want %s", account.OrganizationID, f.org)
}
base := 4
updated, xerr := r.Update(t.Context(), account.OrganizationID.String(), account.ID.String(), &models.UpdateEmail{WarmupBase: &base})
if xerr != nil {
t.Fatal(xerr)
}
if updated.WarmupBase != base {
t.Fatalf("starting volume = %d, want %d", updated.WarmupBase, base)
}
account, xerr = r.Get(t.Context(), f.org.String(), f.account.String())
if xerr != nil || account == nil || account.WarmupBase != base {
t.Fatalf("saved mailbox = %+v, error = %v", account, xerr)
}
if _, xerr := r.Get(t.Context(), uuid.NewString(), f.account.String()); xerr != errx.ErrNotFound {
t.Fatalf("other workspace lookup = %v, want not found", xerr)
}
}
+2 -2
View File
@@ -1308,7 +1308,7 @@ func (r *emailRepository) Search(ctx context.Context, orgID, search string, curs
func (r *emailRepository) Get(ctx context.Context, orgID, emailAccountID string) (*models.Email, *errx.Error) {
query := `
SELECT
ea.id, ea.email, ea.name, ea.signature_plain, ea.signature_html, ea.signature_sync, ea.signature_code, ea.send_as_email,
ea.id, ea.organization_id, ea.email, ea.name, ea.signature_plain, ea.signature_html, ea.signature_sync, ea.signature_code, ea.send_as_email,
ea.provider, ea.mail_host, ea.auth_method, ea.domain_grant_id, ea.vendor_connection_id, COALESCE((SELECT vc.vendor FROM mailbox_vendor_connections vc WHERE vc.id = ea.vendor_connection_id), ''), ea.avatar_url, ea.status, COALESCE(ea.last_synced_at, ea.created_at) AS last_synced_at, ea.last_id, ea.campaign_limit,
ea.min_wait_time, ea.reply_to, ea.tracking_domain, ea.tracking_domain_verified, ea.tracking_domain_verified_at, ea.track_direct_mail,
ea.auth_state, ea.auth_spf, ea.auth_dkim, ea.auth_dmarc, ea.auth_dmarc_policy, ea.auth_reason, ea.auth_checked_at, ea.auth_failing_since,
@@ -1333,7 +1333,7 @@ func (r *emailRepository) Get(ctx context.Context, orgID, emailAccountID string)
query,
params...,
).Scan(
&i.ID, &i.Email, &i.Name, &i.SignaturePlain, &i.SignatureHTML, &i.SignatureSync, &i.SignatureCode, &i.SendAsEmail, &i.Provider, &i.MailHost, &i.AuthMethod, &i.DomainGrantID, &i.VendorConnectionID, &i.Vendor, &i.AvatarURL, &i.Status,
&i.ID, &i.OrganizationID, &i.Email, &i.Name, &i.SignaturePlain, &i.SignatureHTML, &i.SignatureSync, &i.SignatureCode, &i.SendAsEmail, &i.Provider, &i.MailHost, &i.AuthMethod, &i.DomainGrantID, &i.VendorConnectionID, &i.Vendor, &i.AvatarURL, &i.Status,
&i.LastSyncedAt, &i.LastID, &i.CampaignLimit, &i.MinWaitTime, &i.ReplyTo, &i.TrackingDomain, &i.TrackingDomainVerified, &i.TrackingDomainVerifiedAt, &i.TrackDirectMail,
&i.AuthState, &i.AuthSPF, &i.AuthDKIM, &i.AuthDMARC, &i.AuthDMARCPolicy, &i.AuthReason, &i.AuthCheckedAt, &i.AuthFailingSince,
&i.Warmup, &i.WarmupPausedAt, &i.WarmupBase, &i.WarmupMax, &i.WarmupIncrease, &i.WarmupReplyRate, &i.WarmupTag, &i.WarmupPoolType,