165 lines
5.7 KiB
Go
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
|
|
}
|