Files
warmbly/internal/config/constants.go
T

309 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
const (
DefaultColor = "#c4c8cf"
Domain = "warmbly.com"
// LimitMin/LimitMax bound every per-mailbox and per-campaign daily send
// cap the API will store. 5000 covers real provider ceilings (Google
// Workspace 2000/day, M365 10000 recipients/day); the safe cold band
// stays 30-50/day and is steered by defaults, warnings and the advisor.
LimitMin = 0
LimitMax = 5000
CampaignDailyLimitMin = 3
CampaignLimitDefault = 50
MinWaitTimeDefault = 600
WarmupBaseDefault = 10
WarmupMaxDefault = 40
WarmupIncreaseDefault = 1
// Net-new campaign send controls. The ramp mirrors the warmup ramp shape
// (start, +increment/day, ceiling) but is applied only via min() against
// the per-mailbox cold cap, so it can only lower effective volume.
CampaignSenderWeightDefault = 1
CampaignSenderWeightMax = 100
CampaignRampStartDefault = 10
CampaignRampIncrementDefault = 5
CampaignRampCeilingDefault = 50
CampaignMaxNewLeadsMax = 1000
MaxContactSize = 10240
// MaxEmailBodySize bounds a single stored message body. 200 KB cut real
// HTML newsletters mid-document; 512 KB clears the overwhelming majority
// of them while still bounding what one message can cost.
MaxEmailBodySize = 512 * 1024 // 512 KB
MaxEmailFolders = 30
// MaxSearchBodyText bounds the plain-text copy of a message body kept in
// Postgres for full-text search. The body itself lives in object storage;
// this only has to be long enough to find a message by what it says.
MaxSearchBodyText = 16 * 1024 // 16 KB
// ImapFetchBatchSize bounds how many messages one IMAP sync window holds in
// memory, so a large folder is never buffered whole before any body is read.
ImapFetchBatchSize = 200
// Mailbox sync fair use. Connecting a mailbox imports its recent history
// (the backfill), then follows new mail (live). Every number below is a
// default: the four Sync* settings are operator-editable in the admin
// panel's instance settings, the rest are fixed pacing constants for the
// worker-side governor (internal/app/worker/wmail/governor.go).
//
// The governor defers rather than drops: mail over budget waits for the
// next window with its cursor held, and only a flood or repeated daily
// overage deactivates the mailbox. Replies to this mailbox's own sends
// ride a separate priority lane so an outreach reply is never starved by
// a bulky inbox.
SyncBackfillDaysDefault = 90 // how far back the initial import reaches
SyncBackfillDaysMax = 730 // longest window an operator may set
SyncBackfillMessagesDefault = 5_000 // most messages the initial import stores per mailbox
SyncBackfillMessagesMax = 100_000
SyncDailyMessagesMailboxDefault = 2_000 // new (live) messages stored per mailbox per UTC day
SyncDailyMessagesMailboxMax = 100_000
SyncDailyMessagesOrgDefault = 25_000 // new + backfilled messages stored per organization per UTC day
SyncDailyMessagesOrgMax = 2_000_000
SyncBurstPer5Min = 300 // live messages one mailbox may store in any 5 minute window
SyncHourlyPerMailbox = 1_000 // live messages one mailbox may store in any clock hour
SyncBackfillPerMinute = 240 // backfill pacing per mailbox
SyncFloodPerHour = 5_000 // new live messages observed in one hour that mark a mailbox as flooding
SyncThrottleEscalationDays = 3 // throttled UTC days out of the last 7 that deactivate a mailbox
// Sequences. Empty by default so the editor shows a smart, position-based
// label (e.g. "Email 1") until the user names the step themselves.
SequenceDefaultName = ""
SequenceSubjectLimit = 100
SequenceBodyLimit = 30_000
// SequenceWaitAfterMax bounds a step's per-step delay (in days). Mirrors the
// editor's 060 day cap so an API caller can't persist an absurd or negative
// delay that the scheduler would then turn into an unreachable send time.
SequenceWaitAfterMax = 60
// CampaignSendMaxAttempts is how many times one (contact, step) may be
// handed to a worker before the lead is marked failed and dropped from the
// campaign. A worker-reported failure clears the step's sent_at so the next
// tick retries it; this bounds that loop for a mailbox that can never send.
CampaignSendMaxAttempts = 5
// CampaignNotDueGraceSeconds is how far in the future a step's hard
// constraints (wait_after, start date, sending window, mailbox min-gap)
// may sit while a firing task still sends it. Beyond this the scheduler
// reports the step deferred so the task reschedules instead of sending a
// follow-up early; a task that fired on time always passes.
CampaignNotDueGraceSeconds = 60
// CampaignMaxDeferMinutes bounds how far ahead a DEFERRED campaign tick may
// park its successor. A deferral means "nothing is sendable right now", and
// the reasons it says that (no lead is due, the new-lead cap is spent, no
// same-provider mailbox) all change from outside the chain: leads get
// imported, a reply routes a contact onto a live branch, a mailbox comes
// back under budget. A campaign is a single self-perpetuating task, so a
// park at the literal next-due time (days out for a "wait 3 days" step) is
// also the next time anything re-reads that state — which is how a campaign
// with freshly imported leads sits at "Queued / Not started" for days.
// Re-checking on this horizon costs one scheduling pass per idle campaign
// per interval and bounds that staleness. It applies ONLY to deferrals: a
// tick that actually sent parks its successor at the paced interval, which
// is the send spacing and must not be shortened.
CampaignMaxDeferMinutes = 15
// CampaignStaleParkHours is when the reconciler starts distrusting a parked
// wakeup. Even-distribution can only push a successor to the end of the
// mailbox's current day, so a pending tick further out than this was parked
// by a deferral (including ones written before deferrals were capped) and is
// re-checked against the campaign's real next-due time.
CampaignStaleParkHours = 24
// CampaignReparkMarginMinutes is how much earlier the recomputed slot must
// be before the reconciler moves a stale park. Slot selection carries
// jitter, so without a margin a campaign whose window genuinely is days out
// (a Monday-only schedule) would be re-parked a few minutes earlier on every
// pass forever.
CampaignReparkMarginMinutes = 60
// CampaignSendReclaimAfterMinutes is how long a reserved-but-unresolved send
// (dispatched_at set, no worker result, no sent_at) is left alone before the
// reclaimer treats its outcome as lost and walks it back as a failed
// attempt. A live worker answers every SEND_EMAIL within seconds, so this
// only ever fires when the worker died mid-send or the result was lost, and
// it must stay well clear of a slow provider handshake.
CampaignSendReclaimAfterMinutes = 30
// CampaignSendStampAttempts is how many times the control plane retries the
// sent_at stamp after a send is already on the bus. The reservation is what
// keeps the step from being re-sent, so a lost stamp is a pacing problem,
// not a duplicate — but it is still worth a couple of quick retries before
// falling back to the worker result to repair it.
CampaignSendStampAttempts = 3
// Webhook/integration fan-out throttle. Caps how many events of a single
// type one org can fan out to its webhooks + integration sinks
// (Slack/Discord/CRM) per minute — the backstop against a campaign "notify"
// action, or any per-contact event, flooding a customer's endpoints. Over
// the cap, further events of that type in the same minute are dropped
// (logged), not queued.
//
// The effective cap is PLAN-BASED: it scales with the org's resolved mailbox
// allowance (override > plan > hard cap), so bigger plans get more webhook
// throughput. These three knobs are "what we centrally allow":
//
// - Base: a generous floor every org gets, including free/no-plan orgs, so
// normal usage never trips the throttle (good UX by default).
// - PerMailbox: how much each mailbox in the plan's allowance adds, since
// webhook volume tracks sending activity.
// - Max: a hard ceiling so even an "unlimited" plan stays bounded.
//
// Sized far above normally-spaced sending (per-mailbox daily caps + min-gap
// spacing); only a runaway loop or a huge per-contact fan-out approaches it.
WebhookDispatchBasePerMinute = 600 // generous floor for any org (10/s)
WebhookDispatchPerMailboxPerMinute = 30 // added per mailbox the plan allows
WebhookDispatchMaxPerMinute = 6000 // hard ceiling (100/s) for any plan
// Unibox
UniboxLimitMin = 1
UniboxLimitMax = 100
UniboxLimitDefault = 50
// VerificationRecheckDays is how long a verification verdict is trusted
// before the address is checked again. Mailboxes get created and closed;
// a verdict from last quarter is a guess.
VerificationRecheckDays = 90
// VerificationUnknownRecheckDays is the shorter shelf life of an
// inconclusive verdict (greylisted, timeout, undisclosing provider).
VerificationUnknownRecheckDays = 30
// VerificationEvidenceFreshDays is how long real mail to an address (a
// delivery, an open, a reply) excuses it from being re-checked.
VerificationEvidenceFreshDays = 180
// VerificationDeliveryWindowHours is how long after a send with no
// bounce the delivery counts as evidence the mailbox exists.
VerificationDeliveryWindowHours = 72
// VerificationBatchSize is how many contacts one scheduler pass checks.
VerificationBatchSize = 200
// VerificationIntervalSeconds is how often the scheduler passes. A pass
// that finds a full batch runs again immediately, so a large import drains
// at the verifier's speed rather than one batch per interval.
VerificationIntervalSeconds = 60
// VerificationProbeConcurrency bounds parallel in-house SMTP probes.
VerificationProbeConcurrency = 4
// VerificationProviderConcurrency bounds parallel paid-provider lookups.
VerificationProviderConcurrency = 8
// VerificationBreakerWindow and VerificationBreakerInvalidPct are the
// in-house probe's self-check: when this share of the last window of
// probe verdicts is "invalid", the probe itself is suspect (issue #200,
// #264) and its invalid verdicts are filed as unknown for
// VerificationBreakerCooldownMinutes.
VerificationBreakerWindow = 200
VerificationBreakerInvalidPct = 40.0
VerificationBreakerCooldownMinutes = 60
// WarmupVerifyHeader is the custom header carrying the warmup
// verification token on outbound warmup mail. The name is intentionally
// generic (not "X-Warmbly-*") so anti-spam vendors cannot trivially
// cluster on the header name to fingerprint warmup traffic.
WarmupVerifyHeader = "X-Mailtrace-Verify"
// Product-level hard caps. These are the backstop for plans that
// advertise "unlimited" — marketing can keep saying unlimited, but
// the runtime never grants truly unbounded usage. Each cap is the
// floor that GetEffectiveLimits falls back to when both the
// per-org override and the plan column are unset.
//
// Admins can grant strictly larger caps per-org through the
// override flow when there is a legitimate business reason. Growth
// above these defaults goes through the limit-increase request
// workflow so the decision is audited and the org has a paper trail
// acknowledging the new ceiling.
//
// These numbers are deliberately generous enough that ordinary use
// never trips them, and conservative enough that "I want to spin up
// 5,000 mailboxes overnight" can't happen without explicit approval.
HardCapMailboxes = 200 // total connected mailboxes per org
HardCapCampaignsTotal = 500 // total campaigns ever created
HardCapCampaignsActive = 50 // simultaneously active campaigns
HardCapTeamMembers = 100 // seats per org
HardCapContacts = 1_000_000 // contacts per org
HardCapDailyCampaignSends = 1000 // campaign emails per org per day
// Daily creation throttles. The total caps above stop "you have
// 5000 campaigns on this org" — the throttles below stop "you
// created 1000 campaigns today on a fresh unlimited account."
// Different shape: a per-(org, resource, day) Redis counter that
// resets at UTC midnight, decoupled from any plan tier.
//
// These are creation-rate ceilings, not total caps; raising them
// per-org is intentionally not exposed in the override editor
// because the per-day shape protects abuse posture rather than
// product utility.
DailyThrottleNewCampaigns = 20 // new campaigns per org per day
DailyThrottleNewMailboxes = 5 // newly connected mailboxes per org per day
// Pool link: mailboxes a self-hosted instance may enroll in the hosted
// warmup pool without a paid pool plan, and the handshake lifetimes.
PoolLinkCodeTTLMinutes = 15
PoolLinkPollIntervalSeconds = 3
PoolLinkPlanID = "00000000-0000-0000-0000-000000000002"
PoolLinkPlanPriceUSD = 15
WarmupPoolTierFallbackFloor = 25 // below this many same-tier recipients, healthy other-tier mailboxes fill in
WarmupPoolFallbackMinAgeDays = 3 // other-tier mailboxes must be this old before they fill in
DailyThrottleNewOrgs = 3 // new workspaces per owner per day
// DailyThrottleNewScheduledSends caps how many NEW scheduled-send
// schedules a single user can create in a rolling 24h window. The
// real defense against burst abuse — someone writing a loop that
// queues thousands of scheduled sends in seconds. Set high enough
// that no human-driven volume comes close (a power user replying
// to 200 inbound messages a day couldn't hit it organically).
DailyThrottleNewScheduledSends = 1000
// MaxPendingScheduledSendsPerUser caps how many pending scheduled
// email sends one user can have queued at once. The DAILY rate
// (DailyThrottleNewScheduledSends) is the primary abuse defense;
// this is the DB-bloat defense — each pending row carries a body
// (~5KB), so capping pending count keeps total scheduled-queue
// storage bounded per user.
//
// 10,000 is generous: a user scheduling 100 sends/day for the next
// 100 days hits this exactly once. The combination of "1K new/day"
// + "10K total pending" means a legitimate user cannot organically
// hit either, while a scripted attacker is bounded on both axes.
//
// Cloud Tasks cost is negligible at this size — at $0.40/M
// operations, 10K pending = 20K ops = $0.008/user even at the
// hardest abuse. The cap exists for DB sanity, not cost.
//
// Future: per-plan ceiling lookup. Today: single backstop.
MaxPendingScheduledSendsPerUser = 10000
// Undo send: instant sends are queued this many seconds in the
// future so the sender can still cancel. Per-user setting stored in
// users.undo_send_seconds; the migration CHECK mirrors these bounds.
UndoSendSecondsMin = 5
UndoSendSecondsDefault = 30
UndoSendSecondsMax = 120
// Notification email window: how long email-channel notifications hold
// before flushing as one bundled email. Per-user setting; the 30 minute
// floor is deliberate — there is no per-event email mode, so the channel
// can never become a per-notification firehose. Security sign-in alerts
// bypass the window entirely.
NotificationEmailWindowMinMinutes = 30
NotificationEmailWindowDefaultMinutes = 30
NotificationEmailWindowMaxMinutes = 1440
)
// InboundClassificationHeaders are the internet headers the API-based inbound
// sync (Gmail, Microsoft Graph) surfaces into EmailMessageData.Flags as
// "Header:value" pseudo-flags. The sync model carries no arbitrary-header field,
// so this is how the consumer's reply/bounce classifier (replyclassify via
// buildReplyHeaders) sees the machine-reply and delivery-status-report markers.
// From/Subject already ride the envelope; these add the high-signal headers.
var InboundClassificationHeaders = []string{
"Auto-Submitted",
"Precedence",
"Content-Type",
"Return-Path",
"X-Autoreply",
"X-Autorespond",
"X-Auto-Response-Suppress",
}