package email // Repository provides GORM-based persistence for Email channel data. // Reference: Chatwoot uses ActiveRecord for ChannelEmail CRUD with callbacks: // - before_validation :ensure_forward_to_address // - before_create :ensure_imap_connection // - before_create :ensure_smtp_connection // // Design: Repository pattern abstracts GORM operations, matching gochat's // existing repository pattern used by other channel providers. import ( "context" "fmt" channelmodel "github.com/gochat/gochat/internal/model/channel" "github.com/gochat/gochat/internal/model" "gorm.io/gorm" applogger "github.com/gochat/gochat/pkg/logger" ) // Repository handles Email channel database operations. type Repository struct { db *gorm.DB } // NewRepository creates an Email repository with GORM DB. func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } // === CRUD Operations === // Create persists a new ChannelEmail record. // Reference: Chatwoot ChannelEmail.create with before_validation + before_create callbacks func (r *Repository) Create(ctx context.Context, channel *channelmodel.ChannelEmail) error { if err := r.db.WithContext(ctx).Create(channel).Error; err != nil { return fmt.Errorf("failed to create ChannelEmail: %w", err) } applogger.L().Info("Email channel created", "channel_id", channel.ID, "email", channel.Email, ) return nil } // GetByID retrieves a ChannelEmail by its primary key. func (r *Repository) GetByID(ctx context.Context, id uint) (*channelmodel.ChannelEmail, error) { var channel channelmodel.ChannelEmail if err := r.db.WithContext(ctx).First(&channel, id).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("ChannelEmail not found (id=%d)", id) } return nil, fmt.Errorf("failed to get ChannelEmail: %w", err) } return &channel, nil } // GetByEmail retrieves a ChannelEmail by its email address. // Reference: Chatwoot ChannelEmail.find_by_email for inbox lookup func (r *Repository) GetByEmail(ctx context.Context, email string) (*channelmodel.ChannelEmail, error) { var channel channelmodel.ChannelEmail if err := r.db.WithContext(ctx).Where("email = ?", email).First(&channel).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("ChannelEmail not found (email=%s)", email) } return nil, fmt.Errorf("failed to get ChannelEmail by email: %w", err) } return &channel, nil } // Update modifies an existing ChannelEmail record. func (r *Repository) Update(ctx context.Context, channel *channelmodel.ChannelEmail) error { if err := r.db.WithContext(ctx).Save(channel).Error; err != nil { return fmt.Errorf("failed to update ChannelEmail: %w", err) } applogger.L().Info("Email channel updated", "channel_id", channel.ID, "email", channel.Email, ) return nil } // Delete removes a ChannelEmail record. // Reference: Chatwoot ChannelEmail.destroy — no special after_destroy hooks for email func (r *Repository) Delete(ctx context.Context, id uint) error { if err := r.db.WithContext(ctx).Delete(&channelmodel.ChannelEmail{}, id).Error; err != nil { return fmt.Errorf("failed to delete ChannelEmail: %w", err) } applogger.L().Info("Email channel deleted", "channel_id", id, ) return nil } // === Inbox Association === // GetInboxForChannel retrieves the Inbox associated with a ChannelEmail. // Reference: Chatwoot ChannelEmail has one Inbox through channelable func (r *Repository) GetInboxForChannel(ctx context.Context, channelID uint) (*model.Inbox, error) { var inbox model.Inbox if err := r.db.WithContext(ctx). Where("channel_id = ? AND channel_type = ?", channelID, "email"). First(&inbox).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("Inbox not found for ChannelEmail (channel_id=%d)", channelID) } return nil, fmt.Errorf("failed to get Inbox for ChannelEmail: %w", err) } return &inbox, nil } // === List Operations === // ListByAccount retrieves all ChannelEmail records for an account. func (r *Repository) ListByAccount(ctx context.Context, accountID uint) ([]*channelmodel.ChannelEmail, error) { var channels []*channelmodel.ChannelEmail // ChannelEmail doesn't directly have account_id — it's through the Inbox // We need to join through Inbox to filter by account if err := r.db.WithContext(ctx). Joins("JOIN inboxes ON inboxes.channel_id = channel_emails.id AND inboxes.channel_type = 'email'"). Where("inboxes.account_id = ?", accountID). Find(&channels).Error; err != nil { return nil, fmt.Errorf("failed to list ChannelEmail by account: %w", err) } return channels, nil } // === Poll State Tracking === // PollState tracks IMAP polling state per inbox (last UID seen, etc). type PollState struct { InboxID uint `gorm:"primaryKey"` LastUID uint32 `gorm:"column:last_uid"` LastPollAt int64 `gorm:"column:last_poll_at"` // Unix timestamp } // TableName specifies the table name for PollState. func (PollState) TableName() string { return "email_poll_states" } // GetPollState retrieves the IMAP poll state for an inbox. func (r *Repository) GetPollState(ctx context.Context, inboxID uint) (*PollState, error) { var state PollState if err := r.db.WithContext(ctx).First(&state, inboxID).Error; err != nil { if err == gorm.ErrRecordNotFound { // Return default state — no prior poll return &PollState{InboxID: inboxID}, nil } return nil, fmt.Errorf("failed to get poll state: %w", err) } return &state, nil } // UpdatePollState saves the IMAP poll state for an inbox. func (r *Repository) UpdatePollState(ctx context.Context, state *PollState) error { if err := r.db.WithContext(ctx).Save(state).Error; err != nil { return fmt.Errorf("failed to update poll state: %w", err) } return nil }