Files
gochat/internal/channel/email/repository.go
T
2026-06-04 15:44:48 +08:00

165 lines
5.7 KiB
Go

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
}