Documentation
¶
Index ¶
- Constants
- Variables
- func BuildCustomFieldsJSON(fields map[string]string) string
- func BuildDiscordWebhookPayload(event DiscordNotificationEvent) discordWebhookPayload
- func BuildRFCMessage(p EmailParams) string
- func BuildRawMessage(p EmailParams) string
- func CampaignStateTransition(db *sql.DB, name, action, fromStatus, toStatus string) error
- func CheckGWSInstalled() error
- func ConfigPath() string
- func DBPath() string
- func DataDir() string
- func DatabaseDisplayTarget() string
- func DeleteCampaign(db *sql.DB, name string) (int64, error)
- func EnsureDataDir() error
- func EnsureDiscordNotifyCursor(db *sql.DB) error
- func ExtractDomain(email string) string
- func ExtractPlaceholders(s string) []string
- func FormatSendDays(s string) string
- func FormatTickResult(r *TickResult) string
- func GWSAuthLogin(configDir string) error
- func GWSConfigDirForAccount(email string) string
- func GenerateRFCMessageID(fromEmail string) string
- func GetLastPollAt(db *sql.DB) time.Time
- func IsUnsubscribeRequest(subject, snippet string) bool
- func LoadEnvFile(path string) error
- func NormalizeWorkspaceID(workspaceID string) string
- func OpenDB(path string) (*sql.DB, error)
- func ParseSendDays(s string) ([]time.Weekday, error)
- func PrepareFollowUp(p *EmailParams, parentMessageID, threadID, originalSubject string)
- func ProcessBounces(db *sql.DB, gws GWSClient, accounts []Account) (int, error)
- func ProcessDiscordNotifications(ctx context.Context, db *sql.DB, notifier DiscordNotifier, ...) (int, error)
- func ProcessDiscordOperationalNotifications(ctx context.Context, db *sql.DB, notifier DiscordNotifier, ...) (int, error)
- func ProcessIMAPBounces(db *sql.DB, imap IMAPMessageLister, accounts []Account) (int, error)
- func ProcessIMAPReplies(db *sql.DB, imap IMAPMessageLister, accounts []Account) (replies int, unsubscribes int, err error)
- func ProcessReplies(db *sql.DB, gws GWSClient, accounts []Account) (replies int, unsubscribes int, err error)
- func RebalancePendingSchedules(db *sql.DB, accountIDs []int64) error
- func RenderTemplate(tmpl string, fields map[string]string) string
- func ResolveAlias(name string) string
- func ResolveCampaignName(db *sql.DB, nameOrID string) (string, error)
- func ResolveCampaignNameInWorkspace(db *sql.DB, workspaceID, nameOrID string) (string, error)
- func ResolveLeadScheduleTimezone(fields map[string]string, defaultTZ *time.Location) (*time.Location, error)
- func ResolveSecretRef(ref string) (string, error)
- func SenderTemplateFields(fromEmail string) map[string]string
- func SetLastPollAt(db *sql.DB, t time.Time)
- func StripUnresolved(s string) (string, []string)
- func SuggestField(name string, available []string) string
- func UpdateAccount(db *sql.DB, email string, opts UpdateAccountOpts) error
- func UpdateCampaign(db *sql.DB, name string, opts UpdateCampaignOpts) error
- func ValidateEmailParamsHeaders(p EmailParams) error
- func ValidateLeadFields(leads []LeadRecord, placeholders []string) ([]string, error)
- func ValidateLeadScheduleOverrides(leads []LeadRecord) error
- func ValidateSecretRef(ref string) error
- func WorkspaceIDFromEnv() string
- func WriteDefaultConfig(path string) error
- type Account
- type AccountVerifyResult
- type AddAccountResult
- type AddLeadsResult
- type AddLeadsToCampaignOpts
- type AddSMTPIMAPAccountOpts
- type AddSMTPIMAPAccountResult
- type AuditInboxHistoryConfig
- type BackfillEmailMessagesConfig
- type BackfillEmailMessagesResult
- type BlacklistResult
- type Campaign
- type CampaignLead
- type CampaignListRow
- type CampaignPreflightConfig
- type CampaignPreflightHistoryMatch
- type CampaignPreflightResult
- type CampaignPreflightRow
- type CampaignStats
- type CampaignStatusInfo
- type CloneCampaignOpts
- type Config
- type CreateCampaignOpts
- type CreateCampaignResult
- type CreateDraftCampaignOpts
- type DailyLimitWarning
- type Dialect
- type DiscordNotificationEvent
- type DiscordNotifier
- type DiscordNotifyOptions
- type DiscordOperationalNotifyOptions
- type DiscordWebhookNotifier
- type DomainCheck
- type DomainDiagnostic
- type EmailMessage
- type EmailParams
- type EmailValidationPolicy
- type EnvSecretResolver
- type Event
- type EventLogRow
- type FailureReason
- type FindFollowupCandidatesConfig
- type FindNeedsReplyCandidatesConfig
- type FollowupCandidate
- type FollowupCandidatesConfig
- type FollowupCandidatesResult
- type FollowupThreadMessage
- type GWSCLI
- func (g *GWSCLI) GetMessage(account, msgID string) (*GWSMessage, error)
- func (g *GWSCLI) GetThreadMessages(account, threadID string) ([]GWSMessage, error)
- func (g *GWSCLI) ListMessages(account, query string, includeSpamTrash ...bool) ([]GWSMessage, error)
- func (g *GWSCLI) SendEmail(account, to, rawMsg, threadID string) (string, string, error)
- func (g *GWSCLI) SetConfigDir(account, configDir string)
- type GWSClient
- type GWSMessage
- type IMAPAccountVerifier
- type IMAPAuditMessageLister
- type IMAPMessageLister
- type IMAPSentAppender
- type IMAPThreadMessageLister
- type IMAPTransport
- func (t *IMAPTransport) AppendSent(account Account, params EmailParams) (string, error)
- func (t *IMAPTransport) ListAuditMessageHeaders(account Account, since time.Time) ([]GWSMessage, error)
- func (t *IMAPTransport) ListMessages(account Account, since time.Time, includeSpamTrash bool) ([]GWSMessage, error)
- func (t *IMAPTransport) ListThreadMessages(account Account, since time.Time, messageIDs []string) ([]GWSMessage, error)
- func (t *IMAPTransport) VerifyAccount(account Account) error
- type InboxAuditAccountResult
- type InboxAuditMessage
- type InboxAuditResult
- type InboxReconcileResult
- type InboxReplyPreview
- type Lead
- type LeadEmailValidationResult
- type LeadEmailValidationRow
- type LeadForSchedule
- type LeadListRow
- type LeadRecord
- type LeadStatsRow
- type ListAccountsRow
- type ListEmailThreadMessagesOpts
- type NeedsReplyCandidate
- type NeedsReplyCandidatesConfig
- type NeedsReplyCandidatesResult
- type PauseAccountResult
- type PauseLeadResult
- type PlannedEmail
- type PreSendCheckConfig
- type PreviewInboxReplyConfig
- type PreviewRow
- type RecipientVerificationResult
- type RecipientVerifier
- type RemoveLeadResult
- type RenderedEmail
- type ResumeAccountResult
- type ResumeLeadResult
- type RetryCampaignResult
- type SMTPAccountVerifier
- type SMTPEmailSender
- type SMTPIMAPAccountSecrets
- type SMTPRecipientVerifier
- type SMTPTransport
- type ScheduleConfig
- type ScheduledSend
- type ScheduledSendRow
- type SecretResolver
- type SecretResolverFunc
- type SendInboxReplyConfig
- type SendInboxReplyResult
- type SendNowResult
- type SendSMTPTestEmailOpts
- type SendSMTPTestEmailResult
- type Sequence
- type SequenceDefaults
- type SequenceStep
- type SequenceVariant
- type StepStats
- type Store
- func (s *Store) AcquireTickLock(ctx context.Context) (TickLock, error)
- func (s *Store) Begin() (*Tx, error)
- func (s *Store) Close() error
- func (s *Store) DisplayTarget() string
- func (s *Store) Exec(query string, args ...any) (sql.Result, error)
- func (s *Store) Query(query string, args ...any) (*sql.Rows, error)
- func (s *Store) QueryRow(query string, args ...any) *sql.Row
- func (s *Store) Rebind(query string) string
- type SyncEmailThreadConfig
- type SyncEmailThreadResult
- type TickConfig
- type TickLock
- type TickResult
- type Tx
- type UpdateAccountOpts
- type UpdateCampaignOpts
- type UpdateSMTPIMAPAccountOpts
- type VariantStats
Constants ¶
const ( CampaignHistoryScopeAll = "all" CampaignHistoryScopeWorkspace = "workspace" CampaignPreflightReady = "ready" CampaignPreflightManualReview = "manual_review" CampaignPreflightBlocked = "blocked" )
const ( DiscordEventCampaignCompleted = "campaign_completed" DiscordEventCampaignCompletedWithFailures = "campaign_completed_with_failures" DiscordEventSenderIdle = "sender_idle" DiscordEventInboxReconciliationFailed = "inbox_reconciliation_failed" )
const ( EmailValidationPass = "pass" EmailValidationManualReview = "manual_review" EmailValidationFail = "fail" RecipientStatusVerified = "verified" RecipientStatusRejected = "rejected" RecipientStatusCatchAll = "catch_all" RecipientStatusUnknown = "unknown" RecipientStatusNoMX = "no_mx" RecipientStatusFreeMail = "free_email" )
const ( AccountProviderGWS = "gws" AccountProviderSMTPIMAP = "smtp_imap" CampaignStatusCompletedWithFailures = "completed_with_failures" )
const ( EmailMessageDirectionOutbound = "outbound" EmailMessageDirectionInbound = "inbound" EmailMessageTypeSent = "sent" EmailMessageTypeReply = "reply" EmailMessageTypeBounce = "bounce" EmailMessageTypeAutoReply = "auto_reply" EmailMessageTypeUnsubscribe = "unsubscribe" EmailMessageTypeManualReply = "manual_reply" )
const ( DefaultWorkspaceID = "default" WorkspaceEnvVar = "COLD_CLI_WORKSPACE_ID" )
const ScheduleTimezoneField = "schedule_timezone"
Variables ¶
var BuiltinFields = []string{"email", "first_name", "last_name", "company", "domain"}
BuiltinFields are fields always available for template rendering from the leads table.
var DefaultConfig = Config{
DefaultTimezone: "America/New_York",
DefaultDailyLimit: 50,
MinGapSeconds: 90,
MaxGapSeconds: 140,
SendWindowStart: "09:00",
SendWindowEnd: "17:00",
SendDays: "1,2,3,4,5",
UnsubscribeSubject: "Unsubscribe",
}
var ErrContactSuppressed = errors.New("contact is on the do-not-contact list")
var ErrDiscordWorkspaceUnconfigured = errors.New("no Discord webhook configured for workspace")
ErrDiscordWorkspaceUnconfigured means routing deliberately skipped an alert.
var SenderFields = []string{"sender_email", "sender_local", "sender_domain", "sender_name"}
SenderFields are derived from the account assigned to a scheduled send.
Functions ¶
func BuildCustomFieldsJSON ¶
BuildCustomFieldsJSON extracts non-builtin fields from a LeadRecord and returns them as JSON.
func BuildDiscordWebhookPayload ¶
func BuildDiscordWebhookPayload(event DiscordNotificationEvent) discordWebhookPayload
func BuildRFCMessage ¶
func BuildRFCMessage(p EmailParams) string
BuildRFCMessage constructs an RFC 2822 message.
func BuildRawMessage ¶
func BuildRawMessage(p EmailParams) string
BuildRawMessage constructs an RFC 2822 message and returns it as a base64url-encoded string.
func CampaignStateTransition ¶
CampaignStateTransition changes a campaign's status with validation.
func CheckGWSInstalled ¶
func CheckGWSInstalled() error
CheckGWSInstalled verifies gws binary is available on PATH.
func ConfigPath ¶
func ConfigPath() string
func DataDir ¶
func DataDir() string
DataDir returns the cold-cli data directory path. Respects COLD_CLI_DATA_DIR env var for testing.
func DatabaseDisplayTarget ¶
func DatabaseDisplayTarget() string
DatabaseDisplayTarget returns a user-facing database target description.
func DeleteCampaign ¶
DeleteCampaign deletes a campaign and all associated data.
func EnsureDataDir ¶
func EnsureDataDir() error
EnsureDataDir creates the data directory if it doesn't exist.
func EnsureDiscordNotifyCursor ¶
EnsureDiscordNotifyCursor initializes the Discord cursor to the current event high-water mark. Tick calls this before polling so first deploys do not alert on old historical replies, while replies found during that tick still notify.
func ExtractDomain ¶
ExtractDomain returns the domain part of an email address.
func ExtractPlaceholders ¶
ExtractPlaceholders returns all unique {{placeholder}} names from a string.
func FormatSendDays ¶
FormatSendDays converts "1,2,3,4,5" to "Mon-Fri" or similar human-readable format.
func FormatTickResult ¶
func FormatTickResult(r *TickResult) string
FormatTickResult returns a human-readable summary of a tick result.
func GWSAuthLogin ¶
GWSAuthLogin runs 'gws auth login' for a specific account config dir.
func GWSConfigDirForAccount ¶
GWSConfigDirForAccount returns the config dir path for an account. Creates the directory and copies client_secret.json from the default gws config.
func GenerateRFCMessageID ¶
GenerateRFCMessageID creates a Message-ID scoped to the sender domain.
func GetLastPollAt ¶
GetLastPollAt returns the last poll timestamp, or a default (24h ago).
func IsUnsubscribeRequest ¶
IsUnsubscribeRequest checks if a message appears to be an unsubscribe request based on its subject and snippet text.
func LoadEnvFile ¶
LoadEnvFile loads KEY=VALUE pairs from an explicit env file into the process environment. It intentionally does not auto-discover .env files.
func NormalizeWorkspaceID ¶
func ParseSendDays ¶
ParseSendDays converts a comma-separated day string to weekday slice. Accepts numbers (0=Sun,1=Mon,...,6=Sat) or names (sun,mon,...,sat).
func PrepareFollowUp ¶
func PrepareFollowUp(p *EmailParams, parentMessageID, threadID, originalSubject string)
PrepareFollowUp adds threading headers to an EmailParams for step 2+.
func ProcessBounces ¶
ProcessBounces checks inbox messages for bounce NDRs. Uses two strategies:
- Thread matching — if the NDR shares a thread_id with a sent email, we know the lead
- Snippet/header parsing — fallback: extract bounced email from NDR text
Returns the number of new bounces detected.
func ProcessDiscordNotifications ¶
func ProcessDiscordNotifications(ctx context.Context, db *sql.DB, notifier DiscordNotifier, opts DiscordNotifyOptions) (int, error)
ProcessDiscordNotifications sends unnotified reply/unsubscribe events to Discord.
func ProcessDiscordOperationalNotifications ¶
func ProcessDiscordOperationalNotifications(ctx context.Context, db *sql.DB, notifier DiscordNotifier, opts DiscordOperationalNotifyOptions) (int, error)
ProcessDiscordOperationalNotifications sends durable, workspace-scoped campaign completion and sender-idle alerts. Completion alerts include any campaign inboxes that became idle, and suppress a duplicate immediate idle alert for those inboxes. Idle reminders repeat at most once per interval.
func ProcessIMAPBounces ¶
ProcessIMAPBounces checks IMAP messages for bounce NDRs.
func ProcessIMAPReplies ¶
func ProcessIMAPReplies(db *sql.DB, imap IMAPMessageLister, accounts []Account) (replies int, unsubscribes int, err error)
ProcessIMAPReplies checks IMAP inbox messages for replies to sent SMTP/IMAP emails.
func ProcessReplies ¶
func ProcessReplies(db *sql.DB, gws GWSClient, accounts []Account) (replies int, unsubscribes int, err error)
ProcessReplies checks inbox messages for replies to our sent emails. Returns the number of new replies and unsubscribes detected.
func RebalancePendingSchedules ¶
RebalancePendingSchedules rewrites pending send_at timestamps for the affected accounts so daily limits are respected across all active and draft campaigns.
func RenderTemplate ¶
RenderTemplate replaces all {{placeholder}} occurrences with values from the fields map. It also resolves known aliases (e.g., {{name}} → first_name value).
func ResolveAlias ¶
ResolveAlias returns the canonical field name for a known alias, or the name unchanged.
func ResolveCampaignName ¶
ResolveCampaignName accepts a campaign name or numeric ID and returns the campaign name. This lets users reference campaigns by either their name or the ID shown in "campaign list".
func ResolveCampaignNameInWorkspace ¶
ResolveCampaignNameInWorkspace accepts a campaign name or numeric ID and returns the campaign name for one workspace.
func ResolveLeadScheduleTimezone ¶
func ResolveLeadScheduleTimezone(fields map[string]string, defaultTZ *time.Location) (*time.Location, error)
ResolveLeadScheduleTimezone returns the lead's effective scheduling timezone.
func ResolveSecretRef ¶
ResolveSecretRef resolves a secret reference without exposing the value in errors.
func SenderTemplateFields ¶
SenderTemplateFields returns template fields derived from a sending account.
func SetLastPollAt ¶
SetLastPollAt updates the last poll timestamp.
func StripUnresolved ¶
StripUnresolved removes any remaining {{...}} placeholders from a string, collapses double spaces left behind, and returns the names of stripped variables.
func SuggestField ¶
SuggestField returns the closest matching field name if within Levenshtein distance 3, or "".
func UpdateAccount ¶
func UpdateAccount(db *sql.DB, email string, opts UpdateAccountOpts) error
UpdateAccount modifies account settings.
func UpdateCampaign ¶
func UpdateCampaign(db *sql.DB, name string, opts UpdateCampaignOpts) error
UpdateCampaign updates campaign settings with validation.
func ValidateEmailParamsHeaders ¶
func ValidateEmailParamsHeaders(p EmailParams) error
ValidateEmailParamsHeaders rejects control characters that could create additional RFC 5322 headers. Address syntax is validated separately by the caller because display names are allowed in some message sources.
func ValidateLeadFields ¶
func ValidateLeadFields(leads []LeadRecord, placeholders []string) ([]string, error)
ValidateLeadFields checks that every placeholder in the sequence maps to a known field and that every lead has non-empty values for all required placeholders. Returns alias-mapping warnings (if any) and an error if validation fails.
func ValidateLeadScheduleOverrides ¶
func ValidateLeadScheduleOverrides(leads []LeadRecord) error
ValidateLeadScheduleOverrides checks optional per-lead scheduling override fields.
func ValidateSecretRef ¶
ValidateSecretRef verifies that a secret reference uses a supported scheme.
The env: scheme is resolved by the local CLI. The secret: scheme is an opaque hosted-product reference resolved by callers that provide their own SecretResolver.
func WorkspaceIDFromEnv ¶
func WorkspaceIDFromEnv() string
func WriteDefaultConfig ¶
Types ¶
type Account ¶
type Account struct {
ID int64 `json:"id"`
WorkspaceID string `json:"workspace_id"`
Email string `json:"email"`
DailyLimit int `json:"daily_limit"`
LastSendAt *time.Time `json:"last_send_at,omitempty"`
Status string `json:"status"`
Provider string `json:"provider"`
GWSConfigDir string `json:"gws_config_dir,omitempty"`
SMTPHost string `json:"smtp_host,omitempty"`
SMTPPort int `json:"smtp_port,omitempty"`
SMTPUsername string `json:"smtp_username,omitempty"`
SMTPPasswordRef string `json:"smtp_password_ref,omitempty"`
SMTPTLSMode string `json:"smtp_tls_mode,omitempty"`
IMAPHost string `json:"imap_host,omitempty"`
IMAPPort int `json:"imap_port,omitempty"`
IMAPUsername string `json:"imap_username,omitempty"`
IMAPPasswordRef string `json:"imap_password_ref,omitempty"`
IMAPTLSMode string `json:"imap_tls_mode,omitempty"`
}
type AccountVerifyResult ¶
type AccountVerifyResult struct {
Email string `json:"email"`
Provider string `json:"provider"`
SMTPOK bool `json:"smtp_ok,omitempty"`
IMAPOK bool `json:"imap_ok,omitempty"`
SMTPError string `json:"smtp_error,omitempty"`
IMAPError string `json:"imap_error,omitempty"`
}
func VerifySMTPIMAPAccount ¶
func VerifySMTPIMAPAccount(account Account, smtpVerifier SMTPAccountVerifier, imapVerifier IMAPAccountVerifier) (*AccountVerifyResult, error)
type AddAccountResult ¶
type AddAccountResult struct {
ID int64 `json:"id"`
WorkspaceID string `json:"workspace_id"`
Email string `json:"email"`
DailyLimit int `json:"daily_limit"`
Status string `json:"status"`
Provider string `json:"provider"`
GWSConfigDir string `json:"gws_config_dir"`
}
AddAccountResult is returned by AddAccount.
func AddAccount ¶
func AddAccount(db *sql.DB, email string, dailyLimit int, configDir string) (*AddAccountResult, error)
AddAccount inserts a new sending account into the database. If the account was previously removed, it is reactivated with the new settings.
func AddAccountInWorkspace ¶
func AddAccountInWorkspace(db *sql.DB, workspaceID, email string, dailyLimit int, configDir string) (*AddAccountResult, error)
AddAccountInWorkspace inserts a new sending account into a workspace. If the account was previously removed, it is reactivated with the new settings.
type AddLeadsResult ¶
type AddLeadsResult struct {
Campaign string `json:"campaign"`
LeadsAdded int `json:"leads_added"`
LeadsSkipped int `json:"leads_skipped"`
ScheduledSends int `json:"scheduled_sends"`
PreviewOnly bool `json:"preview_only,omitempty"`
WouldReactivate bool `json:"would_reactivate,omitempty"`
Emails []PlannedEmail `json:"emails,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}
AddLeadsResult is returned by AddLeadsToCampaign.
func AddLeadsToCampaign ¶
func AddLeadsToCampaign(db *sql.DB, campaignName, leadsFile, leadsInline string) (*AddLeadsResult, error)
AddLeadsToCampaign adds new leads to an existing campaign and schedules their sends. Pass leadsFile for file path, or leadsInline for inline CSV content (one should be non-empty).
func AddLeadsToCampaignWithOpts ¶
func AddLeadsToCampaignWithOpts(db *sql.DB, opts AddLeadsToCampaignOpts) (*AddLeadsResult, error)
AddLeadsToCampaignWithOpts adds one dated cohort to an existing campaign.
type AddLeadsToCampaignOpts ¶
type AddLeadsToCampaignOpts struct {
CampaignName string
LeadsFile string
LeadsInline string
StartDate string
PreviewOnly bool
Reactivate bool
}
AddLeadsToCampaignOpts controls one cohort added to an existing campaign. Active campaigns whose original start date has arrived require a new explicit StartDate so an import cannot accidentally create immediately-due sends.
type AddSMTPIMAPAccountOpts ¶
type AddSMTPIMAPAccountOpts struct {
WorkspaceID string
Email string
DailyLimit int
SMTPHost string
SMTPPort int
SMTPUsername string
SMTPPasswordRef string
SMTPTLSMode string
IMAPHost string
IMAPPort int
IMAPUsername string
IMAPPasswordRef string
IMAPTLSMode string
}
AddSMTPIMAPAccountOpts holds settings for a generic SMTP/IMAP account.
type AddSMTPIMAPAccountResult ¶
type AddSMTPIMAPAccountResult struct {
ID int64 `json:"id"`
WorkspaceID string `json:"workspace_id"`
Email string `json:"email"`
DailyLimit int `json:"daily_limit"`
Status string `json:"status"`
Provider string `json:"provider"`
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPTLSMode string `json:"smtp_tls_mode"`
IMAPHost string `json:"imap_host"`
IMAPPort int `json:"imap_port"`
IMAPUsername string `json:"imap_username"`
IMAPTLSMode string `json:"imap_tls_mode"`
}
AddSMTPIMAPAccountResult is returned by AddSMTPIMAPAccount.
func AddSMTPIMAPAccount ¶
func AddSMTPIMAPAccount(db *sql.DB, opts AddSMTPIMAPAccountOpts) (*AddSMTPIMAPAccountResult, error)
AddSMTPIMAPAccount inserts a generic SMTP/IMAP sending account. Password fields are stored as references, not raw secret values.
func UpdateSMTPIMAPAccount ¶
func UpdateSMTPIMAPAccount(db *sql.DB, email string, opts UpdateSMTPIMAPAccountOpts) (*AddSMTPIMAPAccountResult, error)
UpdateSMTPIMAPAccount modifies provider settings on a generic SMTP/IMAP account.
type AuditInboxHistoryConfig ¶
type AuditInboxHistoryConfig struct {
DB *sql.DB
WorkspaceID string
Since time.Time
SecretResolver SecretResolver
GWS GWSClient
IMAP IMAPMessageLister
Apply bool
}
type BackfillEmailMessagesResult ¶
type BackfillEmailMessagesResult struct {
Scanned int `json:"scanned"`
Backfilled int `json:"backfilled"`
Sent int `json:"sent"`
Inbound int `json:"inbound"`
Skipped int `json:"skipped"`
Unsupported int `json:"unsupported"`
Failed int `json:"failed"`
DryRun bool `json:"dry_run"`
}
func BackfillEmailMessages ¶
func BackfillEmailMessages(cfg BackfillEmailMessagesConfig) (*BackfillEmailMessagesResult, error)
type BlacklistResult ¶
type BlacklistResult struct {
Target string `json:"target"`
IsDomain bool `json:"is_domain"`
BlacklistedLeads int64 `json:"blacklisted_leads"`
CancelledSends int64 `json:"cancelled_sends"`
}
BlacklistResult is returned by BlacklistLead.
func BlacklistLead ¶
func BlacklistLead(db *sql.DB, target string) (*BlacklistResult, error)
BlacklistLead blacklists a lead by email or all leads on a domain.
type Campaign ¶
type Campaign struct {
ID int64 `json:"id"`
WorkspaceID string `json:"workspace_id"`
Name string `json:"name"`
Status string `json:"status"`
SequenceFile string `json:"sequence_file"`
StopOnReply bool `json:"stop_on_reply"`
StopOnDomainReply bool `json:"stop_on_domain_reply"`
SendWindowStart string `json:"send_window_start"`
SendWindowEnd string `json:"send_window_end"`
SendDays string `json:"send_days"`
Timezone string `json:"timezone"`
MinGapSeconds int `json:"min_gap_seconds"`
MaxGapSeconds int `json:"max_gap_seconds"`
CreatedAt time.Time `json:"created_at"`
}
type CampaignLead ¶
type CampaignListRow ¶
type CampaignListRow struct {
ID int64 `json:"id"`
WorkspaceID string `json:"workspace_id"`
Name string `json:"name"`
Status string `json:"status"`
Leads int `json:"leads"`
Sends int `json:"sends"`
SendWindow string `json:"send_window"`
SendDays string `json:"send_days"`
}
CampaignListRow is a row from ListCampaigns.
func ListCampaigns ¶
func ListCampaigns(db *sql.DB) ([]CampaignListRow, error)
ListCampaigns returns campaigns in the default workspace with lead and send counts.
func ListCampaignsForWorkspace ¶
func ListCampaignsForWorkspace(db *sql.DB, workspaceID string) ([]CampaignListRow, error)
ListCampaignsForWorkspace returns campaigns in one workspace with lead and send counts.
type CampaignPreflightConfig ¶
type CampaignPreflightConfig struct {
DB *sql.DB
WorkspaceID string
HistoryScope string
CheckDomains bool
Records []LeadRecord
SkipEmailValidation bool
EmailVerifier RecipientVerifier
ValidationPolicy EmailValidationPolicy
}
type CampaignPreflightHistoryMatch ¶
type CampaignPreflightHistoryMatch struct {
MatchType string `json:"match_type"`
LeadEmail string `json:"lead_email"`
LeadGlobalStatus string `json:"lead_global_status"`
CampaignID int64 `json:"campaign_id,omitempty"`
CampaignName string `json:"campaign_name,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`
CampaignStatus string `json:"campaign_status,omitempty"`
CampaignLead string `json:"campaign_lead_status,omitempty"`
}
type CampaignPreflightResult ¶
type CampaignPreflightResult struct {
WorkspaceID string `json:"workspace_id"`
HistoryScope string `json:"history_scope"`
CheckDomains bool `json:"check_domains"`
EmailChecks bool `json:"email_checks"`
Checked int `json:"checked"`
Ready int `json:"ready"`
ManualReview int `json:"manual_review"`
Blocked int `json:"blocked"`
Rows []CampaignPreflightRow `json:"rows"`
}
func PreflightCampaignLeads ¶
func PreflightCampaignLeads(cfg CampaignPreflightConfig) (*CampaignPreflightResult, error)
PreflightCampaignLeads combines input duplicate checks, cross-campaign history checks, global suppressions, and optional recipient validation. It is read-only and is intended to run before create, clone, or add-leads.
func (CampaignPreflightResult) HasBlockingRows ¶
func (r CampaignPreflightResult) HasBlockingRows() bool
type CampaignPreflightRow ¶
type CampaignPreflightRow struct {
Email string `json:"email"`
Domain string `json:"domain"`
Status string `json:"status"`
Reasons []string `json:"reasons,omitempty"`
ValidationStatus string `json:"validation_status,omitempty"`
SMTPStatus string `json:"smtp_status,omitempty"`
ValidationDetail string `json:"validation_detail,omitempty"`
History []CampaignPreflightHistoryMatch `json:"history,omitempty"`
}
type CampaignStats ¶
type CampaignStats struct {
Name string `json:"name"`
Status string `json:"status"`
Sent int `json:"sent"`
Replies int `json:"replies"`
Unsubscribes int `json:"unsubscribes"`
Bounces int `json:"bounces"`
}
CampaignStats is a row from GetAllCampaignStats.
func GetAllCampaignStats ¶
func GetAllCampaignStats(db *sql.DB) ([]CampaignStats, error)
GetAllCampaignStats returns sent/replied/bounced counts per campaign.
type CampaignStatusInfo ¶
type CampaignStatusInfo struct {
Name string `json:"name"`
Status string `json:"status"`
Sequence string `json:"sequence"`
Timezone string `json:"timezone"`
SendWindow string `json:"send_window"`
SendDays string `json:"send_days"`
Leads int `json:"leads"`
Accounts int `json:"accounts"`
TotalSends int `json:"total_sends"`
SendCounts map[string]int `json:"send_counts"`
CreatedAt string `json:"created_at"`
ContactedLeads int `json:"contacted_leads"`
UniqueRepliedLeads int `json:"unique_replied_leads"`
ReplyRate *float64 `json:"reply_rate,omitempty"`
NextSendAt *string `json:"next_send_at,omitempty"`
LastSendAt *string `json:"last_send_at,omitempty"`
FailureReasons []FailureReason `json:"failure_reasons,omitempty"`
}
CampaignStatusInfo is returned by GetCampaignStatus.
func GetCampaignStatus ¶
func GetCampaignStatus(db *sql.DB, name string) (*CampaignStatusInfo, error)
GetCampaignStatus returns campaign details and send counts.
type CloneCampaignOpts ¶
type CloneCampaignOpts struct {
WorkspaceID string
SourceName string
NewName string
LeadsFile string
LeadsInline string // inline CSV content (alternative to LeadsFile)
Accounts []string // optional: override accounts; empty = reuse source accounts
StartDate string // optional "YYYY-MM-DD"; empty = now
}
CloneCampaignOpts holds options for CloneCampaign.
type Config ¶
type Config struct {
DefaultTimezone string `yaml:"default_timezone"`
DefaultDailyLimit int `yaml:"default_daily_limit"`
MinGapSeconds int `yaml:"min_gap_seconds"`
MaxGapSeconds int `yaml:"max_gap_seconds"`
SendWindowStart string `yaml:"send_window_start"`
SendWindowEnd string `yaml:"send_window_end"`
SendDays string `yaml:"send_days"`
UnsubscribeHeader bool `yaml:"unsubscribe_header"`
UnsubscribeSubject string `yaml:"unsubscribe_subject"`
PreSendChecks map[string]PreSendCheckConfig `yaml:"pre_send_checks"`
}
func LoadConfig ¶
type CreateCampaignOpts ¶
type CreateCampaignOpts struct {
WorkspaceID string
Name string
SequenceFile string
SequenceInline string // inline YAML content (alternative to SequenceFile)
LeadsFile string
LeadsInline string // inline CSV content (alternative to LeadsFile)
AccountEmails []string
StartDate string // optional "YYYY-MM-DD"; empty = now
SendWindowStart string // optional HH:MM override; empty = config default
SendWindowEnd string // optional HH:MM override; empty = config default
SendDays string // optional send days override for this campaign
Timezone string // optional IANA timezone override; empty = config default
}
CreateCampaignOpts holds options for CreateCampaign.
type CreateCampaignResult ¶
type CreateCampaignResult struct {
ID int64 `json:"id"`
WorkspaceID string `json:"workspace_id"`
Name string `json:"name"`
Status string `json:"status"`
Leads int `json:"leads"`
ScheduledSends int `json:"scheduled_sends"`
Accounts int `json:"accounts"`
Warnings []string `json:"warnings,omitempty"`
}
CreateCampaignResult is returned by CreateCampaign.
func CloneCampaign ¶
func CloneCampaign(db *sql.DB, opts CloneCampaignOpts) (*CreateCampaignResult, error)
CloneCampaign creates a new campaign by copying settings from an existing one with new leads.
func CreateCampaign ¶
func CreateCampaign(db *sql.DB, opts CreateCampaignOpts) (*CreateCampaignResult, error)
CreateCampaign parses sequence+CSV, validates, computes schedule, and inserts everything.
func CreateDraftCampaign ¶
func CreateDraftCampaign(db *sql.DB, opts CreateDraftCampaignOpts) (*CreateCampaignResult, error)
CreateDraftCampaign inserts a draft campaign shell without sequence steps or leads.
type CreateDraftCampaignOpts ¶
type CreateDraftCampaignOpts struct {
WorkspaceID string
Name string
AccountEmails []string
SendWindowStart string
SendWindowEnd string
SendDays string
Timezone string
}
CreateDraftCampaignOpts holds options for CreateDraftCampaign.
type DailyLimitWarning ¶
type DailyLimitWarning struct {
Date string `json:"date"`
Account string `json:"account"`
Scheduled int `json:"scheduled"`
Limit int `json:"limit"`
Overflow int `json:"overflow"`
}
DailyLimitWarning describes a day where scheduled sends exceed an account's daily limit.
func GetDailyLimitWarnings ¶
func GetDailyLimitWarnings(db *sql.DB) ([]DailyLimitWarning, error)
GetDailyLimitWarnings checks all pending sends across all active campaigns for each account and returns warnings for days that exceed the account's daily limit.
type Dialect ¶
type Dialect string
func CurrentDialect ¶
func CurrentDialect() Dialect
CurrentDialect returns the active database dialect from environment.
type DiscordNotificationEvent ¶
type DiscordNotificationEvent struct {
EventID int64
EventType string
Timestamp string
WorkspaceID string
CampaignID int64
CampaignName string
CampaignStatus string
LeadEmail string
LeadCompany string
AccountEmail string
AccountEmails []string
IdleAccountEmails []string
FromEmail string
Subject string
Snippet string
MessageID string
LeadsContacted int
SentCount int
ReplyCount int
UnsubscribeCount int
BounceCount int
FailedCount int
SkippedCount int
CancelledCount int
PendingCount int
IdleSince string
Reminder bool
Recovered bool
}
DiscordNotificationEvent is the compact event shape used for Discord alerts.
type DiscordNotifier ¶
type DiscordNotifier interface {
NotifyDiscord(context.Context, DiscordNotificationEvent) error
}
DiscordNotifier sends a single cold-cli event to Discord.
type DiscordNotifyOptions ¶
DiscordNotifyOptions configures one notification processing pass.
type DiscordOperationalNotifyOptions ¶
type DiscordOperationalNotifyOptions struct {
Workspaces []string
Now time.Time
IdleReminderInterval time.Duration
}
DiscordOperationalNotifyOptions configures campaign completion and sender-idle alerts. Operational alerts are disabled when Workspaces is empty.
type DiscordWebhookNotifier ¶
type DiscordWebhookNotifier struct {
WebhookURL string
// A non-nil map enables explicit routing with no shared-webhook fallback.
WorkspaceWebhookURLs map[string]string
Username string
AvatarURL string
HTTPClient *http.Client
}
DiscordWebhookNotifier posts cold-cli notifications to a Discord webhook URL.
func (DiscordWebhookNotifier) NotifyDiscord ¶
func (n DiscordWebhookNotifier) NotifyDiscord(ctx context.Context, event DiscordNotificationEvent) error
type DomainCheck ¶
type DomainCheck struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Detail string `json:"detail"`
Fix string `json:"fix,omitempty"`
}
DomainCheck is the result of checking one DNS aspect.
type DomainDiagnostic ¶
type DomainDiagnostic struct {
Domain string `json:"domain"`
Checks []DomainCheck `json:"checks"`
Score int `json:"score"`
MaxScore int `json:"max_score"`
}
DomainDiagnostic is the full result of CheckDomain.
func CheckDomain ¶
func CheckDomain(domain string) (*DomainDiagnostic, error)
CheckDomain runs DNS diagnostics for email deliverability.
type EmailMessage ¶
type EmailMessage struct {
ID int64 `json:"id"`
CampaignID int64 `json:"campaign_id"`
LeadID int64 `json:"lead_id"`
AccountID int64 `json:"account_id"`
Direction string `json:"direction"`
Type string `json:"type"`
StepNumber int `json:"step_number"`
ScheduledSendID *int64 `json:"scheduled_send_id,omitempty"`
EventID *int64 `json:"event_id,omitempty"`
MessageID string `json:"message_id"`
ThreadID string `json:"thread_id"`
InReplyTo string `json:"in_reply_to,omitempty"`
FromEmail string `json:"from_email"`
ToEmails string `json:"to_emails"`
CcEmails string `json:"cc_emails,omitempty"`
BccEmails string `json:"bcc_emails,omitempty"`
ReplyToEmails string `json:"reply_to_emails,omitempty"`
Subject string `json:"subject"`
TextBody string `json:"text_body"`
DisplayBody string `json:"display_body"`
DisplayHTML string `json:"display_html"`
HTMLBody string `json:"html_body"`
Snippet string `json:"snippet"`
RawHeaders string `json:"raw_headers,omitempty"`
OccurredAt time.Time `json:"occurred_at"`
CreatedAt time.Time `json:"created_at"`
}
func ListEmailThreadMessages ¶
func ListEmailThreadMessages(db *sql.DB, opts ListEmailThreadMessagesOpts) ([]EmailMessage, error)
type EmailParams ¶
type EmailParams struct {
FromName string
FromEmail string
ToEmail string
CcEmails []string
Subject string
Body string
// For follow-ups (step 2+)
InReplyTo string // Message-ID of the previous step
References string // same as InReplyTo for simple chains
ThreadID string // Gmail thread ID for threading
MessageID string // RFC Message-ID to set before sending
// Unsubscribe
UnsubscribeEmail string // mailto address for List-Unsubscribe header
UnsubscribeSubject string // subject for the mailto unsubscribe
// Optional Date header. If zero, no Date header is added.
Date time.Time
// Stripped unresolved template variables (for logging)
StrippedVars []string
}
EmailParams holds everything needed to construct and send an email.
func BuildEmailForSend ¶
func BuildEmailForSend( seq *Sequence, stepNumber int, variantIndex int, lead map[string]string, fromEmail string, ) EmailParams
BuildEmailForSend constructs the full email for a scheduled send, applying template rendering and selecting the correct variant.
type EmailValidationPolicy ¶
type EnvSecretResolver ¶
type EnvSecretResolver struct{}
EnvSecretResolver resolves env:NAME references from the process environment.
func (EnvSecretResolver) ResolveSecret ¶
func (EnvSecretResolver) ResolveSecret(ref string) (string, error)
type Event ¶
type Event struct {
ID int64 `json:"id"`
CampaignID int64 `json:"campaign_id"`
LeadID int64 `json:"lead_id"`
AccountID int64 `json:"account_id"`
Type string `json:"type"`
StepNumber int `json:"step_number"`
MessageID string `json:"message_id"`
ThreadID string `json:"thread_id"`
Timestamp time.Time `json:"timestamp"`
Metadata string `json:"metadata,omitempty"`
}
type EventLogRow ¶
type EventLogRow struct {
Timestamp string `json:"timestamp"`
Type string `json:"type"`
Campaign string `json:"campaign"`
LeadEmail string `json:"lead_email"`
AccountEmail string `json:"account_email"`
StepNumber int `json:"step_number"`
MessageID string `json:"message_id,omitempty"`
}
EventLogRow is a row from GetEventLog.
func GetEventLog ¶
GetEventLog returns the most recent events, optionally filtered by campaign.
type FailureReason ¶
FailureReason is an error message and its count from failed sends.
type FollowupCandidate ¶
type FollowupCandidate struct {
Rank int `json:"rank"`
CampaignID int64 `json:"campaign_id"`
CampaignName string `json:"campaign_name"`
CampaignStatus string `json:"campaign_status"`
LeadID int64 `json:"lead_id"`
LeadStatus string `json:"lead_status"`
LeadEmail string `json:"lead_email"`
Company string `json:"company,omitempty"`
AccountID int64 `json:"account_id"`
FromEmail string `json:"from_email"`
ToEmail string `json:"to_email"`
Subject string `json:"subject"`
ThreadID string `json:"thread_id"`
ReplyCount int `json:"reply_count"`
FollowupCount int `json:"followup_count"`
MessageCount int `json:"message_count"`
AgeDays int `json:"age_days"`
LastInboundAt time.Time `json:"last_inbound_at"`
LastInboundFrom string `json:"last_inbound_from"`
LastInboundBody string `json:"last_inbound_body"`
LastOutboundAt time.Time `json:"last_outbound_at"`
LastOutboundBody string `json:"last_outbound_body"`
Thread []FollowupThreadMessage `json:"thread,omitempty"`
}
func FindFollowupCandidates ¶
func FindFollowupCandidates(cfg FindFollowupCandidatesConfig) ([]FollowupCandidate, error)
type FollowupCandidatesResult ¶
type FollowupCandidatesResult struct {
WorkspaceID string `json:"workspace_id"`
Since time.Time `json:"since"`
MinAgeHours int `json:"min_age_hours"`
MaxFollowups int `json:"max_followups"`
Audit *InboxAuditResult `json:"audit"`
Reconciliation *InboxReconcileResult `json:"reconciliation,omitempty"`
Candidates []FollowupCandidate `json:"candidates"`
}
func ReviewFollowupCandidates ¶
func ReviewFollowupCandidates(cfg FollowupCandidatesConfig) (*FollowupCandidatesResult, error)
ReviewFollowupCandidates makes the provider audit a mandatory gate. It never returns candidates from stale state. Reconcile is the only mode that writes, and it imports provider-confirmed messages without sending email.
type FollowupThreadMessage ¶
type GWSCLI ¶
type GWSCLI struct {
Timeout time.Duration
ConfigDirs map[string]string // account email → gws config dir
}
GWSCLI is the real implementation that calls gws as a subprocess.
func ConfiguredGWSClient ¶
ConfiguredGWSClient loads per-account OAuth config directories from the cold-cli database for CLI and hosted worker entrypoints.
func (*GWSCLI) GetMessage ¶
func (g *GWSCLI) GetMessage(account, msgID string) (*GWSMessage, error)
GetMessage retrieves a single message with full payload (headers).
func (*GWSCLI) GetThreadMessages ¶
func (g *GWSCLI) GetThreadMessages(account, threadID string) ([]GWSMessage, error)
func (*GWSCLI) ListMessages ¶
func (g *GWSCLI) ListMessages(account, query string, includeSpamTrash ...bool) ([]GWSMessage, error)
ListMessages lists messages matching a Gmail search query.
func (*GWSCLI) SendEmail ¶
SendEmail sends an email via gws and returns the Gmail message ID and thread ID.
func (*GWSCLI) SetConfigDir ¶
SetConfigDir registers a gws config directory for a specific account.
type GWSClient ¶
type GWSClient interface {
SendEmail(account, to, rawMsg, threadID string) (msgID, sentThreadID string, err error)
ListMessages(account, query string, includeSpamTrash ...bool) ([]GWSMessage, error)
GetMessage(account, msgID string) (*GWSMessage, error)
GetThreadMessages(account, threadID string) ([]GWSMessage, error)
}
GWSClient is the interface for Gmail operations via gws CLI.
type GWSMessage ¶
type GWSMessage struct {
ID string `json:"id"`
ThreadID string `json:"threadId"`
Snippet string `json:"snippet"`
TextBody string
HTMLBody string
MimeType string
PartMimeTypes []string
LabelIDs []string `json:"labelIds"`
Headers map[string]string // parsed from payload.headers
From string
To string
Subject string
InReplyTo string
Date time.Time
}
GWSMessage represents a parsed Gmail message from gws output.
func ParseIMAPRawMessage ¶
type IMAPAccountVerifier ¶
IMAPAccountVerifier verifies IMAP connectivity and authentication.
type IMAPAuditMessageLister ¶
type IMAPAuditMessageLister interface {
ListAuditMessageHeaders(account Account, since time.Time) ([]GWSMessage, error)
}
type IMAPMessageLister ¶
type IMAPMessageLister interface {
ListMessages(account Account, since time.Time, includeSpamTrash bool) ([]GWSMessage, error)
}
IMAPMessageLister lists mailbox messages for reply and bounce polling.
type IMAPSentAppender ¶
type IMAPSentAppender interface {
AppendSent(account Account, params EmailParams) (mailbox string, err error)
}
IMAPSentAppender stores an SMTP-delivered message in the account's Sent mailbox so webmail and other IMAP clients show the same conversation.
type IMAPThreadMessageLister ¶
type IMAPTransport ¶
type IMAPTransport struct {
Resolver SecretResolver
Timeout time.Duration
Mailboxes []string
SpamTrashBoxes []string
SentMailboxes []string
MaxBodyBytes int64
// contains filtered or unexported fields
}
IMAPTransport is the production IMAP polling transport.
func NewIMAPTransport ¶
func NewIMAPTransport(resolver SecretResolver) *IMAPTransport
func (*IMAPTransport) AppendSent ¶
func (t *IMAPTransport) AppendSent(account Account, params EmailParams) (string, error)
func (*IMAPTransport) ListAuditMessageHeaders ¶
func (t *IMAPTransport) ListAuditMessageHeaders(account Account, since time.Time) ([]GWSMessage, error)
ListAuditMessageHeaders reads only RFC headers from every selectable mailbox. Matching locally avoids incomplete server-side HEADER searches and keeps historical audits much lighter than downloading every message body.
func (*IMAPTransport) ListMessages ¶
func (t *IMAPTransport) ListMessages(account Account, since time.Time, includeSpamTrash bool) ([]GWSMessage, error)
func (*IMAPTransport) ListThreadMessages ¶
func (t *IMAPTransport) ListThreadMessages(account Account, since time.Time, messageIDs []string) ([]GWSMessage, error)
ListThreadMessages searches Inbox and Sent server-side by RFC threading headers so refreshing one conversation does not download every message in the account since the campaign began.
func (*IMAPTransport) VerifyAccount ¶
func (t *IMAPTransport) VerifyAccount(account Account) error
type InboxAuditAccountResult ¶
type InboxAuditMessage ¶
type InboxAuditMessage struct {
CampaignID int64 `json:"campaign_id"`
LeadID int64 `json:"lead_id"`
AccountID int64 `json:"account_id"`
AccountEmail string `json:"account_email"`
Provider string `json:"provider"`
Direction string `json:"direction"`
Type string `json:"type"`
MessageID string `json:"message_id"`
ThreadID string `json:"thread_id"`
From string `json:"from"`
To string `json:"to"`
Subject string `json:"subject"`
OccurredAt time.Time `json:"occurred_at"`
}
type InboxAuditResult ¶
type InboxAuditResult struct {
WorkspaceID string `json:"workspace_id"`
Since time.Time `json:"since"`
Scanned int `json:"scanned"`
Matched int `json:"matched"`
Missing int `json:"missing"`
Applied int `json:"applied"`
Accounts []InboxAuditAccountResult `json:"accounts"`
Messages []InboxAuditMessage `json:"messages"`
}
func AuditInboxHistory ¶
func AuditInboxHistory(cfg AuditInboxHistoryConfig) (*InboxAuditResult, error)
type InboxReconcileResult ¶
type InboxReconcileResult struct {
WorkspaceID string `json:"workspace_id"`
Since time.Time `json:"since"`
Discovered int `json:"discovered"`
Applied int `json:"applied"`
Remaining int `json:"remaining"`
ApplyAudit *InboxAuditResult `json:"apply_audit"`
Verification *InboxAuditResult `json:"verification"`
}
func ReconcileInboxHistory ¶
func ReconcileInboxHistory(cfg AuditInboxHistoryConfig) (*InboxReconcileResult, error)
ReconcileInboxHistory imports provider-confirmed campaign thread messages, then performs a second read-only audit. Callers must use the verification result, not the apply pass, as the clean-state gate.
type InboxReplyPreview ¶
type InboxReplyPreview struct {
CampaignID int64 `json:"campaign_id"`
LeadID int64 `json:"lead_id"`
AccountID int64 `json:"account_id"`
FromName string `json:"from_name,omitempty"`
FromEmail string `json:"from_email"`
ToEmail string `json:"to_email"`
CcEmails []string `json:"cc_emails,omitempty"`
Subject string `json:"subject"`
Body string `json:"body"`
InReplyTo string `json:"in_reply_to"`
References string `json:"references"`
ThreadID string `json:"thread_id"`
LatestDirection string `json:"latest_direction"`
LatestType string `json:"latest_type"`
IdempotencyKey string `json:"idempotency_key"`
Warnings []string `json:"warnings,omitempty"`
}
func PreviewInboxReply ¶
func PreviewInboxReply(cfg PreviewInboxReplyConfig) (*InboxReplyPreview, error)
type Lead ¶
type Lead struct {
ID int64 `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Company string `json:"company"`
Domain string `json:"domain"`
CustomFields string `json:"custom_fields,omitempty"`
GlobalStatus string `json:"global_status"`
CreatedAt time.Time `json:"created_at"`
}
type LeadEmailValidationResult ¶
type LeadEmailValidationResult struct {
Rows []LeadEmailValidationRow `json:"rows"`
Checked int `json:"checked"`
Pass int `json:"pass"`
ManualReview int `json:"manual_review"`
Fail int `json:"fail"`
}
func ValidateLeadEmails ¶
func ValidateLeadEmails(records []LeadRecord, verifier RecipientVerifier, policy EmailValidationPolicy) (*LeadEmailValidationResult, error)
func (LeadEmailValidationResult) HasBlockingRows ¶
func (r LeadEmailValidationResult) HasBlockingRows() bool
type LeadEmailValidationRow ¶
type LeadForSchedule ¶
LeadForSchedule is the minimal lead info needed for scheduling.
type LeadListRow ¶
type LeadListRow struct {
ID int64 `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
Company string `json:"company"`
Domain string `json:"domain"`
GlobalStatus string `json:"global_status"`
Campaigns int `json:"campaigns"`
}
LeadListRow is a row from ListLeads.
type LeadRecord ¶
LeadRecord represents a parsed CSV row with all fields available by header name.
func ParseLeadsCSV ¶
func ParseLeadsCSV(path string) ([]LeadRecord, []string, error)
ParseLeadsCSV reads a CSV file and returns parsed lead records. Headers are normalized to lowercase with spaces replaced by underscores. Strips UTF-8 BOM if present.
func ParseLeadsCSVFromReader ¶
func ParseLeadsCSVFromReader(r io.Reader) ([]LeadRecord, []string, error)
ParseLeadsCSVFromReader parses leads CSV from a reader. Returns the lead records and the normalized header names.
type LeadStatsRow ¶
type LeadStatsRow struct {
Email string `json:"email"`
Status string `json:"status"`
StepsSent int `json:"steps_sent"`
ReplyAt *string `json:"reply_at,omitempty"`
}
LeadStatsRow is a row from GetCampaignLeadStats.
func GetCampaignLeadStats ¶
func GetCampaignLeadStats(db *sql.DB, campaignID int64) ([]LeadStatsRow, error)
GetCampaignLeadStats returns per-lead stats for a campaign.
type ListAccountsRow ¶
type ListAccountsRow struct {
ID int64 `json:"id"`
WorkspaceID string `json:"workspace_id"`
Email string `json:"email"`
DailyLimit int `json:"daily_limit"`
Status string `json:"status"`
Provider string `json:"provider"`
}
ListAccountsRow is a row from ListAccounts.
func ListAccounts ¶
func ListAccounts(db *sql.DB) ([]ListAccountsRow, error)
ListAccounts returns accounts in the default workspace ordered by ID.
func ListAccountsForWorkspace ¶
func ListAccountsForWorkspace(db *sql.DB, workspaceID string) ([]ListAccountsRow, error)
ListAccountsForWorkspace returns accounts in one workspace ordered by ID.
func ListAllAccounts ¶
func ListAllAccounts(db *sql.DB) ([]ListAccountsRow, error)
ListAllAccounts returns all accounts ordered by workspace then ID.
type NeedsReplyCandidate ¶
type NeedsReplyCandidate struct {
Rank int `json:"rank"`
CampaignID int64 `json:"campaign_id"`
CampaignName string `json:"campaign_name"`
CampaignStatus string `json:"campaign_status"`
LeadID int64 `json:"lead_id"`
LeadStatus string `json:"lead_status"`
LeadEmail string `json:"lead_email"`
Company string `json:"company,omitempty"`
AccountID int64 `json:"account_id"`
FromEmail string `json:"from_email"`
ToEmail string `json:"to_email"`
Subject string `json:"subject"`
ThreadID string `json:"thread_id"`
ReplyCount int `json:"reply_count"`
MessageCount int `json:"message_count"`
AgeHours int `json:"age_hours"`
LastInboundAt time.Time `json:"last_inbound_at"`
LastInboundFrom string `json:"last_inbound_from"`
LastInboundBody string `json:"last_inbound_body"`
PreviousOutboundAt time.Time `json:"previous_outbound_at,omitempty"`
PreviousOutboundBody string `json:"previous_outbound_body,omitempty"`
Thread []FollowupThreadMessage `json:"thread,omitempty"`
}
func FindNeedsReplyCandidates ¶
func FindNeedsReplyCandidates(cfg FindNeedsReplyCandidatesConfig) ([]NeedsReplyCandidate, error)
type NeedsReplyCandidatesResult ¶
type NeedsReplyCandidatesResult struct {
WorkspaceID string `json:"workspace_id"`
Since time.Time `json:"since"`
Audit *InboxAuditResult `json:"audit"`
Reconciliation *InboxReconcileResult `json:"reconciliation,omitempty"`
Candidates []NeedsReplyCandidate `json:"candidates"`
}
func ReviewNeedsReplyCandidates ¶
func ReviewNeedsReplyCandidates(cfg NeedsReplyCandidatesConfig) (*NeedsReplyCandidatesResult, error)
ReviewNeedsReplyCandidates makes provider state authoritative before returning threads whose newest message is a human inbound reply. It never drafts or sends. Reconcile is the only mode that writes.
type PauseAccountResult ¶
type PauseAccountResult struct {
Email string `json:"email"`
CancelledSends int64 `json:"cancelled_sends"`
}
PauseAccountResult is returned by PauseAccount.
func PauseAccount ¶
func PauseAccount(db *sql.DB, email string) (*PauseAccountResult, error)
PauseAccount deactivates an account and cancels its pending sends.
func RemoveAccount ¶
func RemoveAccount(db *sql.DB, email string) (*PauseAccountResult, error)
RemoveAccount deactivates an account permanently and cancels its pending sends. The account row is kept (status='removed') because historical sends/events reference it.
type PauseLeadResult ¶
type PauseLeadResult struct {
Email string `json:"email"`
PausedCampaigns int64 `json:"paused_campaigns"`
CancelledSends int64 `json:"cancelled_sends"`
}
PauseLeadResult is returned by PauseLead.
type PlannedEmail ¶
type PlannedEmail struct {
StepNumber int `json:"step_number"`
VariantIndex int `json:"variant_index"`
SendAt string `json:"send_at"`
LeadEmail string `json:"lead_email"`
AccountEmail string `json:"account_email"`
FromName string `json:"from_name"`
Subject string `json:"subject"`
Body string `json:"body"`
StrippedVars []string `json:"stripped_vars,omitempty"`
}
PlannedEmail is an exact rendered email and schedule produced by an add-leads preview transaction.
type PreSendCheckConfig ¶
type PreSendCheckConfig struct {
Command []string `yaml:"command"`
TimeoutSeconds int `yaml:"timeout_seconds"`
}
PreSendCheckConfig runs a trusted executable without a shell. Stdin contains one recipient email or known phone number per line, matching contact-check CLIs. Exit 0 permits delivery, 2 means suppressed, all other results fail closed.
type PreviewInboxReplyConfig ¶
type PreviewRow ¶
type PreviewRow struct {
StepNumber int `json:"step_number"`
VariantIndex int `json:"variant_index"`
SendAt string `json:"send_at"`
Status string `json:"status"`
LeadEmail string `json:"lead_email"`
AccountEmail string `json:"account_email"`
ErrorMessage string `json:"error_message,omitempty"`
}
PreviewRow is a row from GetCampaignPreview.
func GetCampaignPreview ¶
func GetCampaignPreview(db *sql.DB, name string) (campaignID int64, status string, preview []PreviewRow, err error)
GetCampaignPreview returns the full scheduled send list for a campaign.
type RecipientVerifier ¶
type RemoveLeadResult ¶
type RemoveLeadResult struct {
Email string `json:"email"`
Campaign string `json:"campaign"`
CancelledSends int64 `json:"cancelled_sends"`
}
RemoveLeadResult is returned by RemoveLeadFromCampaign.
func RemoveLeadFromCampaign ¶
func RemoveLeadFromCampaign(db *sql.DB, campaignName, email string) (*RemoveLeadResult, error)
RemoveLeadFromCampaign removes a single lead from a specific campaign.
type RenderedEmail ¶
type RenderedEmail struct {
StepNumber int `json:"step_number"`
VariantIndex int `json:"variant_index"`
LeadEmail string `json:"lead_email"`
AccountEmail string `json:"account_email"`
FromName string `json:"from_name"`
Subject string `json:"subject"`
Body string `json:"body"`
StrippedVars []string `json:"stripped_vars,omitempty"`
}
RenderedEmail is a preview of an actual email with templates filled in.
func GetCampaignRenderedPreview ¶
GetCampaignRenderedPreview returns rendered emails for a specific lead (or the first lead) in a campaign.
type ResumeAccountResult ¶
type ResumeAccountResult struct {
Email string `json:"email"`
RestoredSends int64 `json:"restored_sends"`
}
ResumeAccountResult is returned by ResumeAccount.
func ResumeAccount ¶
func ResumeAccount(db *sql.DB, email string) (*ResumeAccountResult, error)
ResumeAccount reactivates a paused account and restores its eligible sends.
type ResumeLeadResult ¶
type ResumeLeadResult struct {
Email string `json:"email"`
ResumedCampaigns int64 `json:"resumed_campaigns"`
RestoredSends int64 `json:"restored_sends"`
}
ResumeLeadResult is returned by ResumeLead.
func ResumeLead ¶
func ResumeLead(db *sql.DB, email string) (*ResumeLeadResult, error)
ResumeLead resumes a paused lead: reactivates campaign_leads and restores cancelled sends.
type RetryCampaignResult ¶
RetryCampaignResult is returned by RetryCampaign.
func RetryCampaign ¶
RetryCampaign resets failed sends back to pending with send_at = now. If step is non-nil, only retry failed sends for that specific step.
type SMTPAccountVerifier ¶
SMTPAccountVerifier verifies SMTP connectivity and authentication.
type SMTPEmailSender ¶
type SMTPEmailSender interface {
SendEmail(account Account, params EmailParams) (messageID string, threadID string, err error)
}
SMTPEmailSender sends rendered emails through generic SMTP accounts.
type SMTPIMAPAccountSecrets ¶
func ResolveSMTPIMAPAccountSecrets ¶
func ResolveSMTPIMAPAccountSecrets(account Account, resolver SecretResolver) (*SMTPIMAPAccountSecrets, error)
ResolveSMTPIMAPAccountSecrets resolves the password references for an SMTP/IMAP account.
type SMTPRecipientVerifier ¶
func (SMTPRecipientVerifier) VerifyRecipients ¶
func (v SMTPRecipientVerifier) VerifyRecipients(ctx context.Context, domain string, emails []string) (*RecipientVerificationResult, error)
type SMTPTransport ¶
type SMTPTransport struct {
Resolver SecretResolver
Timeout time.Duration
Now func() time.Time
// contains filtered or unexported fields
}
SMTPTransport is the production SMTP sender.
func NewSMTPTransport ¶
func NewSMTPTransport(resolver SecretResolver) *SMTPTransport
func (*SMTPTransport) SendEmail ¶
func (s *SMTPTransport) SendEmail(account Account, params EmailParams) (string, string, error)
func (*SMTPTransport) VerifyAccount ¶
func (s *SMTPTransport) VerifyAccount(account Account) error
type ScheduleConfig ¶
type ScheduleConfig struct {
CampaignID int64
AccountIDs []int64
Leads []LeadForSchedule
Sequence *Sequence
StartDate string
SendWindowStart string // "09:00"
SendWindowEnd string // "17:00"
SendDays []time.Weekday
Timezone *time.Location
MinGapSeconds int
MaxGapSeconds int
StartTime time.Time // when the campaign starts (typically now)
}
ScheduleConfig holds parameters for schedule computation.
type ScheduledSend ¶
type ScheduledSend struct {
ID int64 `json:"id"`
CampaignID int64 `json:"campaign_id"`
LeadID int64 `json:"lead_id"`
AccountID int64 `json:"account_id"`
StepNumber int `json:"step_number"`
VariantIndex int `json:"variant_index"`
SendAt time.Time `json:"send_at"`
Status string `json:"status"`
ThreadID string `json:"thread_id,omitempty"`
ParentMessageID string `json:"parent_message_id,omitempty"`
MessageID string `json:"message_id,omitempty"`
SentAt *time.Time `json:"sent_at,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
type ScheduledSendRow ¶
type ScheduledSendRow struct {
CampaignID int64
LeadID int64
AccountID int64
StepNumber int
VariantIndex int
SendAt time.Time
}
ScheduledSendRow is a row to insert into scheduled_sends.
func ComputeSchedule ¶
func ComputeSchedule(cfg ScheduleConfig) ([]ScheduledSendRow, error)
ComputeSchedule generates all scheduled_sends rows for a campaign.
type SecretResolver ¶
SecretResolver resolves stored secret references into runtime secret values.
type SecretResolverFunc ¶
SecretResolverFunc adapts a function to SecretResolver.
func (SecretResolverFunc) ResolveSecret ¶
func (fn SecretResolverFunc) ResolveSecret(ref string) (string, error)
type SendInboxReplyConfig ¶
type SendInboxReplyConfig struct {
DB *sql.DB
WorkspaceID string
CampaignID int64
LeadID int64
Subject string
Body string
ReplyAll bool
IdempotencyKey string
Now time.Time
SecretResolver SecretResolver
GWS GWSClient
IMAP IMAPMessageLister
SMTPSender SMTPEmailSender
SentAppender IMAPSentAppender
RefreshThread bool
}
type SendInboxReplyResult ¶
type SendInboxReplyResult struct {
CampaignID int64 `json:"campaign_id"`
LeadID int64 `json:"lead_id"`
AccountID int64 `json:"account_id"`
FromName string `json:"from_name,omitempty"`
FromEmail string `json:"from_email"`
ToEmail string `json:"to_email"`
CcEmails []string `json:"cc_emails,omitempty"`
Subject string `json:"subject"`
MessageID string `json:"message_id"`
ThreadID string `json:"thread_id"`
SentMailbox string `json:"sent_mailbox,omitempty"`
IdempotencyKey string `json:"idempotency_key"`
AlreadySent bool `json:"already_sent"`
Warnings []string `json:"warnings,omitempty"`
}
func SendInboxReply ¶
func SendInboxReply(cfg SendInboxReplyConfig) (*SendInboxReplyResult, error)
type SendNowResult ¶
SendNowResult is returned by SendNowCampaign.
func SendNowCampaign ¶
func SendNowCampaign(db *sql.DB, name string) (*SendNowResult, error)
SendNowCampaign sets send_at to now for all pending sends in a campaign, so the next tick picks them up immediately.
type SendSMTPTestEmailOpts ¶
type SendSMTPTestEmailOpts struct {
RecipientEmail string `json:"recipient_email"`
Subject string `json:"subject"`
Body string `json:"body"`
}
SendSMTPTestEmailOpts holds a one-off SMTP test email request.
type SendSMTPTestEmailResult ¶
type SendSMTPTestEmailResult struct {
Email string `json:"email"`
RecipientEmail string `json:"recipient_email"`
MessageID string `json:"message_id"`
ThreadID string `json:"thread_id"`
}
SendSMTPTestEmailResult is returned after a test email is accepted by SMTP.
func SendSMTPTestEmail ¶
func SendSMTPTestEmail(account Account, opts SendSMTPTestEmailOpts, resolver SecretResolver) (*SendSMTPTestEmailResult, error)
SendSMTPTestEmail sends a one-off test email through an SMTP/IMAP account.
type Sequence ¶
type Sequence struct {
Name string `yaml:"name"`
Defaults SequenceDefaults `yaml:"defaults"`
Steps []SequenceStep `yaml:"steps"`
}
Sequence represents a parsed sequence YAML file.
func ParseSequence ¶
ParseSequence reads and parses a sequence YAML file.
func ParseSequenceFromBytes ¶
ParseSequenceFromBytes parses sequence YAML from bytes.
func (*Sequence) CollectPlaceholders ¶
CollectPlaceholders returns all unique placeholders used across all steps and variants.
type SequenceDefaults ¶
type SequenceDefaults struct {
FromName string `yaml:"from_name"`
}
type SequenceStep ¶
type SequenceStep struct {
Step int `yaml:"step"`
Delay int `yaml:"delay"` // days after previous step
Subject string `yaml:"subject"`
Body string `yaml:"body"`
Variants []SequenceVariant `yaml:"variants"`
}
type SequenceVariant ¶
type StepStats ¶
type StepStats struct {
Step int `json:"step"`
Sent int `json:"sent"`
Replies int `json:"replies"`
Unsubscribes int `json:"unsubscribes"`
Bounces int `json:"bounces"`
}
StepStats is a row from GetCampaignStepStats.
type Store ¶
Store is a thin wrapper around the database handle plus dialect metadata and dialect-specific tick locking.
func (*Store) AcquireTickLock ¶
AcquireTickLock acquires the dialect-specific tick lock and returns a handle that must be closed to release it.
func (*Store) DisplayTarget ¶
DisplayTarget returns a user-facing database target description.
type SyncEmailThreadConfig ¶
type SyncEmailThreadConfig struct {
DB *sql.DB
WorkspaceID string
CampaignID int64
LeadID int64
ThreadID string
DryRun bool
SecretResolver SecretResolver
GWS GWSClient
IMAP IMAPMessageLister
}
SyncEmailThreadConfig refreshes one campaign/lead conversation from its provider mailbox before an operator reviews or replies to it.
type SyncEmailThreadResult ¶
type SyncEmailThreadResult struct {
CampaignID int64 `json:"campaign_id"`
LeadID int64 `json:"lead_id"`
AccountID int64 `json:"account_id"`
AccountEmail string `json:"account_email"`
Provider string `json:"provider"`
ThreadID string `json:"thread_id"`
Fetched int `json:"fetched"`
Matched int `json:"matched"`
Added int `json:"added"`
Updated int `json:"updated"`
InboundAdded int `json:"inbound_added"`
OutboundAdded int `json:"outbound_added"`
Stored int `json:"stored"`
DryRun bool `json:"dry_run"`
}
func SyncEmailThread ¶
func SyncEmailThread(cfg SyncEmailThreadConfig) (*SyncEmailThreadResult, error)
type TickConfig ¶
type TickConfig struct {
DB *sql.DB
GWS GWSClient
DryRun bool
SendNow bool // ignore send_at timestamps, send all pending
Now time.Time // injectable for testing
NoSleep bool // skip inter-send sleep (for testing)
Timezone *time.Location // for daily limit day boundary; defaults to UTC
UnsubscribeHeader bool // add List-Unsubscribe header (off by default for cold email)
UnsubscribeSubject string // subject for List-Unsubscribe mailto header
SecretResolver SecretResolver // optional resolver for SMTP/IMAP password refs
SMTPSender SMTPEmailSender
IMAP IMAPMessageLister
DiscordNotifier DiscordNotifier
DiscordNotifyLimit int
DiscordProviders []string
DiscordOperationalWorkspaces []string
}
TickConfig holds configuration for a tick invocation.
type TickResult ¶
type TickResult struct {
RepliesDetected int `json:"replies_detected"`
UnsubscribesDetected int `json:"unsubscribes_detected"`
BouncesDetected int `json:"bounces_detected"`
DiscordNotificationsSent int `json:"discord_notifications_sent"`
Sent int `json:"sent"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
DryRun bool `json:"dry_run"`
}
TickResult holds the summary of a tick invocation.
func Tick ¶
func Tick(cfg TickConfig) (*TickResult, error)
Tick runs one tick cycle: poll replies, poll bounces, send due emails.
type UpdateAccountOpts ¶
type UpdateAccountOpts struct {
DailyLimit *int
}
UpdateAccountOpts holds fields to update on an account.
type UpdateCampaignOpts ¶
type UpdateCampaignOpts struct {
StartDate *string
SendWindowStart *string
SendWindowEnd *string
SendDays *string
Timezone *string
MinGapSeconds *int
MaxGapSeconds *int
SequenceFile *string // path to new sequence YAML
}
UpdateCampaignOpts holds fields to update. Zero values are ignored.
type UpdateSMTPIMAPAccountOpts ¶
type UpdateSMTPIMAPAccountOpts struct {
DailyLimit *int
SMTPHost *string
SMTPPort *int
SMTPUsername *string
SMTPPasswordRef *string
SMTPTLSMode *string
IMAPHost *string
IMAPPort *int
IMAPUsername *string
IMAPPasswordRef *string
IMAPTLSMode *string
}
UpdateSMTPIMAPAccountOpts holds fields to update on a generic SMTP/IMAP account.
type VariantStats ¶
type VariantStats struct {
Step int `json:"step"`
Variant int `json:"variant"`
Sent int `json:"sent"`
Replies int `json:"replies"`
ReplyRate float64 `json:"reply_rate"`
Unsubscribes int `json:"unsubscribes"`
Bounces int `json:"bounces"`
}
VariantStats is a row from GetCampaignVariantStats.
func GetCampaignVariantStats ¶
func GetCampaignVariantStats(db *sql.DB, campaignID int64) ([]VariantStats, error)
GetCampaignVariantStats returns per-step, per-variant stats for a campaign.
Source Files
¶
- account.go
- account_verify.go
- campaign.go
- campaign_preflight.go
- config.go
- csv.go
- db.go
- discord_notify.go
- email_body.go
- email_message.go
- email_message_backfill.go
- email_thread_sync.go
- email_validation.go
- env_file.go
- gws.go
- imap.go
- inbox_audit.go
- inbox_followups.go
- inbox_needs_reply.go
- lead.go
- lead_fields.go
- models.go
- pre_send_check.go
- reply.go
- reply_body.go
- schedule_overrides.go
- scheduler.go
- secrets.go
- send.go
- smtp.go
- sql_runner.go
- stats.go
- store.go
- template.go
- test_email.go
- tick.go
- workspace.go