mirror of
https://github.com/ghostersk/gowebmail.git
synced 2026-04-17 08:36:01 +01:00
189 lines
4.7 KiB
Go
189 lines
4.7 KiB
Go
// 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" {
|
|
days = 36500 // ~100 years = full mailbox
|
|
}
|
|
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
|
|
}
|
|
|
|
days := account.SyncDays
|
|
if days <= 0 || account.SyncMode == "all" {
|
|
days = 36500 // ~100 years = full mailbox
|
|
}
|
|
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
|
|
}
|