Files
gowebmail/internal/syncer/syncer.go

194 lines
4.9 KiB
Go
Raw Normal View History

2026-03-07 06:20:39 +00:00
// Package syncer provides background IMAP synchronisation for all active accounts.
package syncer
import (
"context"
"fmt"
"log"
"time"
"github.com/yourusername/gomail/internal/db"
"github.com/yourusername/gomail/internal/email"
"github.com/yourusername/gomail/internal/models"
)
// Scheduler runs background sync for all active accounts according to their
// individual sync_interval settings.
type Scheduler struct {
db *db.DB
stop chan struct{}
}
// New creates a new Scheduler. Call Start() to begin background syncing.
func New(database *db.DB) *Scheduler {
return &Scheduler{db: database, stop: make(chan struct{})}
}
// Start launches the scheduler goroutine. Ticks every minute and checks
// which accounts are due for sync based on last_sync and sync_interval.
func (s *Scheduler) Start() {
go func() {
log.Println("Background sync scheduler started")
s.runDue()
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.runDue()
case <-s.stop:
log.Println("Background sync scheduler stopped")
return
}
}
}()
}
// Stop signals the scheduler to exit.
func (s *Scheduler) Stop() {
close(s.stop)
}
func (s *Scheduler) runDue() {
accounts, err := s.db.ListAllActiveAccounts()
if err != nil {
log.Printf("Sync scheduler: list accounts: %v", err)
return
}
now := time.Now()
for _, account := range accounts {
if account.SyncInterval <= 0 {
continue
}
nextSync := account.LastSync.Add(time.Duration(account.SyncInterval) * time.Minute)
if account.LastSync.IsZero() || now.After(nextSync) {
go s.syncAccount(account)
}
}
}
// SyncAccountNow performs an immediate sync of one account. Returns messages synced.
func (s *Scheduler) SyncAccountNow(accountID int64) (int, error) {
account, err := s.db.GetAccount(accountID)
if err != nil || account == nil {
return 0, fmt.Errorf("account %d not found", accountID)
}
return s.doSync(account)
}
// SyncFolderNow syncs a single folder for an account.
func (s *Scheduler) SyncFolderNow(accountID, folderID int64) (int, error) {
account, err := s.db.GetAccount(accountID)
if err != nil || account == nil {
return 0, fmt.Errorf("account %d not found", accountID)
}
folder, err := s.db.GetFolderByID(folderID)
if err != nil || folder == nil || folder.AccountID != accountID {
return 0, fmt.Errorf("folder %d not found", folderID)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
c, err := email.Connect(ctx, account)
if err != nil {
return 0, err
}
defer c.Close()
days := account.SyncDays
if days <= 0 || account.SyncMode == "all" {
2026-03-07 20:29:20 +00:00
days = 0 // 0 = fetch ALL via IMAP ALL criteria
2026-03-07 06:20:39 +00:00
}
messages, err := c.FetchMessages(folder.FullPath, days)
if err != nil {
return 0, err
}
synced := 0
for _, msg := range messages {
msg.FolderID = folder.ID
if err := s.db.UpsertMessage(msg); err == nil {
synced++
}
}
s.db.UpdateFolderCounts(folder.ID)
s.db.UpdateAccountLastSync(accountID)
return synced, nil
}
func (s *Scheduler) syncAccount(account *models.EmailAccount) {
synced, err := s.doSync(account)
if err != nil {
log.Printf("Sync [%s]: %v", account.EmailAddress, err)
s.db.SetAccountError(account.ID, err.Error())
s.db.WriteAudit(nil, models.AuditAppError,
"sync error for "+account.EmailAddress+": "+err.Error(), "", "")
return
}
s.db.ClearAccountError(account.ID)
if synced > 0 {
log.Printf("Synced %d messages for %s", synced, account.EmailAddress)
}
}
func (s *Scheduler) doSync(account *models.EmailAccount) (int, error) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
c, err := email.Connect(ctx, account)
if err != nil {
return 0, err
}
defer c.Close()
mailboxes, err := c.ListMailboxes()
if err != nil {
return 0, fmt.Errorf("list mailboxes: %w", err)
}
synced := 0
for _, mb := range mailboxes {
folderType := email.InferFolderType(mb.Name, mb.Attributes)
folder := &models.Folder{
AccountID: account.ID,
Name: mb.Name,
FullPath: mb.Name,
FolderType: folderType,
}
if err := s.db.UpsertFolder(folder); err != nil {
log.Printf("Upsert folder %s: %v", mb.Name, err)
continue
}
dbFolder, _ := s.db.GetFolderByPath(account.ID, mb.Name)
if dbFolder == nil {
continue
}
// Skip folders that the user has disabled sync on
if !dbFolder.SyncEnabled {
continue
}
2026-03-07 06:20:39 +00:00
days := account.SyncDays
if days <= 0 || account.SyncMode == "all" {
2026-03-07 20:29:20 +00:00
days = 0 // 0 = fetch ALL via IMAP ALL criteria
2026-03-07 06:20:39 +00:00
}
messages, err := c.FetchMessages(mb.Name, days)
if err != nil {
log.Printf("Fetch %s/%s: %v", account.EmailAddress, mb.Name, err)
continue
}
for _, msg := range messages {
msg.FolderID = dbFolder.ID
if err := s.db.UpsertMessage(msg); err == nil {
synced++
}
}
s.db.UpdateFolderCounts(dbFolder.ID)
}
s.db.UpdateAccountLastSync(account.ID)
return synced, nil
}