internal

package
v0.0.0-...-96abca7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2026 License: MIT Imports: 42 Imported by: 0

Documentation

Index

Constants

View Source
const (
	AccountProviderGWS      = "gws"
	AccountProviderSMTPIMAP = "smtp_imap"
)
View Source
const (
	EmailMessageDirectionOutbound = "outbound"
	EmailMessageDirectionInbound  = "inbound"

	EmailMessageTypeSent        = "sent"
	EmailMessageTypeReply       = "reply"
	EmailMessageTypeUnsubscribe = "unsubscribe"
	EmailMessageTypeManualReply = "manual_reply"
)
View Source
const ScheduleTimezoneField = "schedule_timezone"

Variables

View Source
var BuiltinFields = []string{"email", "first_name", "last_name", "company", "domain"}

BuiltinFields are fields always available for template rendering from the leads table.

View Source
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",
}

Functions

func BuildCustomFieldsJSON

func BuildCustomFieldsJSON(fields map[string]string) string

BuildCustomFieldsJSON extracts non-builtin fields from a LeadRecord and returns them as JSON.

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

func CampaignStateTransition(db *sql.DB, name, action, fromStatus, toStatus string) error

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 DBPath

func DBPath() string

DBPath returns the database file path.

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

func DeleteCampaign(db *sql.DB, name string) (int64, error)

DeleteCampaign deletes a campaign and all associated data.

func EnsureDataDir

func EnsureDataDir() error

EnsureDataDir creates the data directory if it doesn't exist.

func ExtractDomain

func ExtractDomain(email string) string

ExtractDomain returns the domain part of an email address.

func ExtractPlaceholders

func ExtractPlaceholders(s string) []string

ExtractPlaceholders returns all unique {{placeholder}} names from a string.

func FormatSendDays

func FormatSendDays(s string) string

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

func GWSAuthLogin(configDir string) error

GWSAuthLogin runs 'gws auth login' for a specific account config dir.

func GWSConfigDirForAccount

func GWSConfigDirForAccount(email string) string

GWSConfigDirForAccount returns the config dir path for an account. Creates the directory and copies client_secret.json from the default gws config.

func GenerateRFCMessageID

func GenerateRFCMessageID(fromEmail string) string

GenerateRFCMessageID creates a Message-ID scoped to the sender domain.

func GetLastPollAt

func GetLastPollAt(db *sql.DB) time.Time

GetLastPollAt returns the last poll timestamp, or a default (24h ago).

func IsUnsubscribeRequest

func IsUnsubscribeRequest(subject, snippet string) bool

IsUnsubscribeRequest checks if a message appears to be an unsubscribe request based on its subject and snippet text.

func LoadEnvFile

func LoadEnvFile(path string) error

LoadEnvFile loads KEY=VALUE pairs from an explicit env file into the process environment. It intentionally does not auto-discover .env files.

func OpenDB

func OpenDB(path string) (*sql.DB, error)

OpenDB opens (or creates) the SQLite database and runs migrations.

func ParseSendDays

func ParseSendDays(s string) ([]time.Weekday, error)

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

func ProcessBounces(db *sql.DB, gws GWSClient, accounts []Account) (int, error)

ProcessBounces checks inbox messages for bounce NDRs. Uses two strategies:

  1. Thread matching — if the NDR shares a thread_id with a sent email, we know the lead
  2. Snippet/header parsing — fallback: extract bounced email from NDR text

Returns the number of new bounces detected.

func ProcessIMAPBounces

func ProcessIMAPBounces(db *sql.DB, imap IMAPMessageLister, accounts []Account) (int, error)

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

func RebalancePendingSchedules(db *sql.DB, accountIDs []int64) error

RebalancePendingSchedules rewrites pending send_at timestamps for the affected accounts so daily limits are respected across all active and draft campaigns.

func RenderTemplate

func RenderTemplate(tmpl string, fields map[string]string) string

RenderTemplate replaces all {{placeholder}} occurrences with values from the fields map. It also resolves known aliases (e.g., {{name}} → first_name value).

func ResolveAlias

func ResolveAlias(name string) string

ResolveAlias returns the canonical field name for a known alias, or the name unchanged.

func ResolveCampaignName

func ResolveCampaignName(db *sql.DB, nameOrID string) (string, error)

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 ResolveLeadScheduleTimezone

func ResolveLeadScheduleTimezone(fields map[string]string, defaultTZ *time.Location) (*time.Location, error)

ResolveLeadScheduleTimezone returns the lead's effective scheduling timezone.

func ResolveSecretRef

func ResolveSecretRef(ref string) (string, error)

ResolveSecretRef resolves a secret reference without exposing the value in errors.

func SetLastPollAt

func SetLastPollAt(db *sql.DB, t time.Time)

SetLastPollAt updates the last poll timestamp.

func StripUnresolved

func StripUnresolved(s string) (string, []string)

StripUnresolved removes any remaining {{...}} placeholders from a string, collapses double spaces left behind, and returns the names of stripped variables.

func SuggestField

func SuggestField(name string, available []string) string

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 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

func ValidateSecretRef(ref string) error

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 WriteDefaultConfig

func WriteDefaultConfig(path string) error

Types

type Account

type Account struct {
	ID              int64      `json:"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"`
}

func GetAccountByEmail

func GetAccountByEmail(db *sql.DB, email string) (Account, error)

GetAccountByEmail loads a full account record by email.

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"`
	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.

type AddLeadsResult

type AddLeadsResult struct {
	Campaign       string   `json:"campaign"`
	LeadsAdded     int      `json:"leads_added"`
	LeadsSkipped   int      `json:"leads_skipped"`
	ScheduledSends int      `json:"scheduled_sends"`
	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).

type AddSMTPIMAPAccountOpts

type AddSMTPIMAPAccountOpts struct {
	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"`
	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 BackfillEmailMessagesConfig

type BackfillEmailMessagesConfig struct {
	DB          *sql.DB
	GWS         GWSClient
	Since       time.Time
	Limit       int
	DryRun      bool
	IncludeSent 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"`
}

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"`
	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 CampaignLead struct {
	CampaignID int64      `json:"campaign_id"`
	LeadID     int64      `json:"lead_id"`
	Status     string     `json:"status"`
	StartedAt  *time.Time `json:"started_at,omitempty"`
}

type CampaignListRow

type CampaignListRow struct {
	ID         int64  `json:"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 all campaigns with lead and send counts.

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"`
	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 {
	SourceName  string
	NewName     string
	LeadsFile   string
	LeadsInline string   // inline CSV content (alternative to LeadsFile)
	Accounts    []string // optional: override accounts; empty = reuse source accounts
}

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"`
}

func LoadConfig

func LoadConfig() (*Config, error)

type CreateCampaignOpts

type CreateCampaignOpts struct {
	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"`
	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 {
	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
const (
	DialectSQLite   Dialect = "sqlite"
	DialectPostgres Dialect = "postgres"
)

func CurrentDialect

func CurrentDialect() Dialect

CurrentDialect returns the active database dialect from environment.

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"`
	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
	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 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

func GetEventLog(db *sql.DB, campaignName string, limit int) ([]EventLogRow, error)

GetEventLog returns the most recent events, optionally filtered by campaign.

type FailureReason

type FailureReason struct {
	Error string `json:"error"`
	Count int    `json:"count"`
}

FailureReason is an error message and its count from failed sends.

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 NewGWSCLI

func NewGWSCLI() *GWSCLI

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

func (g *GWSCLI) SendEmail(account, to, rawMsg, threadID string) (string, string, error)

SendEmail sends an email via gws and returns the Gmail message ID and thread ID.

func (*GWSCLI) SetConfigDir

func (g *GWSCLI) SetConfigDir(account, configDir string)

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
	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

func ParseIMAPRawMessage(accountEmail, mailbox string, uid uint32, raw []byte, envelope *imap.Envelope) GWSMessage

type IMAPAccountVerifier

type IMAPAccountVerifier interface {
	VerifyAccount(account Account) error
}

IMAPAccountVerifier verifies IMAP connectivity and authentication.

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 IMAPTransport

type IMAPTransport struct {
	Resolver SecretResolver
	Timeout  time.Duration

	Mailboxes      []string
	SpamTrashBoxes []string
	MaxBodyBytes   int64
	// contains filtered or unexported fields
}

IMAPTransport is the production IMAP polling transport.

func NewIMAPTransport

func NewIMAPTransport(resolver SecretResolver) *IMAPTransport

func (*IMAPTransport) ListMessages

func (t *IMAPTransport) ListMessages(account Account, since time.Time, includeSpamTrash bool) ([]GWSMessage, error)

func (*IMAPTransport) VerifyAccount

func (t *IMAPTransport) VerifyAccount(account Account) 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 LeadForSchedule

type LeadForSchedule struct {
	ID     int64
	Fields map[string]string
}

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.

func ListLeads

func ListLeads(db *sql.DB, domain, status string, limit int) ([]LeadListRow, error)

ListLeads returns leads, optionally filtered by domain or status.

type LeadRecord

type LeadRecord struct {
	Fields map[string]string
}

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"`
	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 all accounts ordered by ID.

type ListEmailThreadMessagesOpts

type ListEmailThreadMessagesOpts struct {
	CampaignID int64
	LeadID     int64
	ThreadID   string
	Limit      int
}

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.

func PauseLead

func PauseLead(db *sql.DB, email string) (*PauseLeadResult, error)

PauseLead pauses a lead across all campaigns and cancels pending sends.

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 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"`
	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

func GetCampaignRenderedPreview(db *sql.DB, name string, leadEmail string) ([]RenderedEmail, error)

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

type RetryCampaignResult struct {
	Campaign string `json:"campaign"`
	Retried  int    `json:"retried"`
}

RetryCampaignResult is returned by RetryCampaign.

func RetryCampaign

func RetryCampaign(db *sql.DB, name string, step *int) (*RetryCampaignResult, error)

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

type SMTPAccountVerifier interface {
	VerifyAccount(account Account) error
}

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

type SMTPIMAPAccountSecrets struct {
	SMTPPassword string
	IMAPPassword string
}

func ResolveSMTPIMAPAccountSecrets

func ResolveSMTPIMAPAccountSecrets(account Account, resolver SecretResolver) (*SMTPIMAPAccountSecrets, error)

ResolveSMTPIMAPAccountSecrets resolves the password references for an SMTP/IMAP account.

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

type SecretResolver interface {
	ResolveSecret(ref string) (string, error)
}

SecretResolver resolves stored secret references into runtime secret values.

type SecretResolverFunc

type SecretResolverFunc func(ref string) (string, error)

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
	CampaignID     int64
	LeadID         int64
	Subject        string
	Body           string
	Now            time.Time
	SecretResolver SecretResolver
	GWS            GWSClient
	SMTPSender     SMTPEmailSender
}

type SendInboxReplyResult

type SendInboxReplyResult struct {
	CampaignID int64  `json:"campaign_id"`
	LeadID     int64  `json:"lead_id"`
	AccountID  int64  `json:"account_id"`
	FromEmail  string `json:"from_email"`
	ToEmail    string `json:"to_email"`
	Subject    string `json:"subject"`
	MessageID  string `json:"message_id"`
	ThreadID   string `json:"thread_id"`
}

type SendNowResult

type SendNowResult struct {
	Campaign string `json:"campaign"`
	Updated  int    `json:"updated"`
}

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

func ParseSequence(path string) (*Sequence, error)

ParseSequence reads and parses a sequence YAML file.

func ParseSequenceFromBytes

func ParseSequenceFromBytes(data []byte) (*Sequence, error)

ParseSequenceFromBytes parses sequence YAML from bytes.

func (*Sequence) CollectPlaceholders

func (s *Sequence) CollectPlaceholders() []string

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 SequenceVariant struct {
	Subject string `yaml:"subject"`
	Body    string `yaml:"body"`
}

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.

func GetCampaignStepStats

func GetCampaignStepStats(db *sql.DB, campaignID int64) ([]StepStats, error)

GetCampaignStepStats returns per-step stats for a campaign.

type Store

type Store struct {
	DB      *sql.DB
	Dialect Dialect
	// contains filtered or unexported fields
}

Store is a thin wrapper around the database handle plus dialect metadata and dialect-specific tick locking.

func OpenStore

func OpenStore() (*Store, error)

OpenStore opens the configured database and bootstraps the current schema.

func (*Store) AcquireTickLock

func (s *Store) AcquireTickLock(ctx context.Context) (TickLock, error)

AcquireTickLock acquires the dialect-specific tick lock and returns a handle that must be closed to release it.

func (*Store) Begin

func (s *Store) Begin() (*Tx, error)

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database handle.

func (*Store) DisplayTarget

func (s *Store) DisplayTarget() string

DisplayTarget returns a user-facing database target description.

func (*Store) Exec

func (s *Store) Exec(query string, args ...any) (sql.Result, error)

func (*Store) Query

func (s *Store) Query(query string, args ...any) (*sql.Rows, error)

func (*Store) QueryRow

func (s *Store) QueryRow(query string, args ...any) *sql.Row

func (*Store) Rebind

func (s *Store) Rebind(query string) string

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
}

TickConfig holds configuration for a tick invocation.

type TickLock

type TickLock interface {
	Close() error
}

type TickResult

type TickResult struct {
	RepliesDetected      int  `json:"replies_detected"`
	UnsubscribesDetected int  `json:"unsubscribes_detected"`
	BouncesDetected      int  `json:"bounces_detected"`
	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 Tx

type Tx struct {
	*sql.Tx
	// contains filtered or unexported fields
}

func (*Tx) Exec

func (tx *Tx) Exec(query string, args ...any) (sql.Result, error)

func (*Tx) Query

func (tx *Tx) Query(query string, args ...any) (*sql.Rows, error)

func (*Tx) QueryRow

func (tx *Tx) QueryRow(query string, args ...any) *sql.Row

type UpdateAccountOpts

type UpdateAccountOpts struct {
	DailyLimit *int
}

UpdateAccountOpts holds fields to update on an account.

type UpdateCampaignOpts

type UpdateCampaignOpts struct {
	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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL