Files
ai-mailer/cmd/paraclub-ai-mailer/main.go

246 lines
7.6 KiB
Go

package main
import (
"flag"
"os"
"os/signal"
"syscall"
"time"
"paraclub-ai-mailer/config"
"paraclub-ai-mailer/internal/ai"
"paraclub-ai-mailer/internal/fetcher"
"paraclub-ai-mailer/internal/imap"
"paraclub-ai-mailer/internal/logger"
"paraclub-ai-mailer/internal/tokens"
"github.com/sirupsen/logrus"
)
func main() {
configPath := flag.String("config", "config.yaml", "Path to configuration file")
flag.Parse()
// Load configuration
logger.Debug("Loading configuration from:", *configPath)
cfg, err := config.Load(*configPath)
if err != nil {
logger.WithError(err).Error("Failed to load configuration")
panic(err)
}
logger.WithFields(logrus.Fields{
"logLevel": cfg.Logging.Level,
"logFile": cfg.Logging.FilePath,
}).Debug("Configuration loaded successfully")
// Initialize logger
if err := logger.Init(cfg.Logging.Level, cfg.Logging.FilePath); err != nil {
logger.WithError(err).Error("Failed to initialize logger")
panic(err)
}
logger.Debug("Logger initialized successfully")
// Initialize components
logger.WithFields(logrus.Fields{
"server": cfg.IMAP.Server,
"port": cfg.IMAP.Port,
"username": cfg.IMAP.Username,
"mailboxIn": cfg.IMAP.MailboxIn,
}).Debug("Initializing IMAP client")
imapClient, err := imap.New(cfg.IMAP)
if err != nil {
logger.WithError(err).Error("Failed to initialize IMAP client")
os.Exit(1)
}
defer imapClient.Close()
fetcher := fetcher.New()
aiProcessor := ai.New(cfg.AI)
tokenCounter, err := tokens.New()
if err != nil {
logger.WithError(err).Error("Failed to initialize token counter")
os.Exit(1)
}
logger.Debug("All components initialized successfully")
// Setup signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
done := make(chan bool, 1)
logger.Debug("Signal handlers configured")
// Start processing loop
logger.WithField("pollingInterval", cfg.Polling.Interval).Info("Starting email processing loop")
go func() {
ticker := time.NewTicker(cfg.Polling.Interval)
defer ticker.Stop()
for {
select {
case <-done:
logger.Debug("Received shutdown signal in processing loop")
return
case <-ticker.C:
logger.Debug("Processing tick started")
processEmails(imapClient, fetcher, aiProcessor, tokenCounter, cfg)
logger.Debug("Processing tick completed")
}
}
}()
// Wait for shutdown signal
sig := <-sigChan
logger.WithField("signal", sig.String()).Info("Received shutdown signal")
done <- true
logger.Info("Application shutdown complete")
}
func processEmails(imapClient *imap.IMAPClient, fetcher *fetcher.Fetcher, aiProcessor *ai.AI, tokenCounter *tokens.TokenCounter, cfg *config.Config) {
logger.Debug("Starting email processing cycle")
// Fetch unprocessed emails
emails, err := imapClient.FetchUnprocessedEmails()
if err != nil {
logger.WithError(err).Error("Failed to fetch emails")
return
}
logger.WithField("emailCount", len(emails)).Debug("Fetched unprocessed emails")
if len(emails) == 0 {
logger.Debug("No new emails to process")
return
}
// Fetch context from configured URLs
logger.WithField("urlCount", len(cfg.Context.URLs)).Debug("Fetching context from configured URLs")
contextContent, err := fetcher.FetchAllURLs(cfg.Context.URLs)
if err != nil {
logger.WithError(err).Error("Failed to fetch context content")
return
}
logger.WithField("contextCount", len(contextContent)).Debug("Successfully fetched context content")
// Process each email
var processedCount, errorCount, skippedCount int
for _, email := range emails {
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"from": email.From,
"messageId": email.ID,
}).Info("Processing email")
// Extract clean email content (removes attachments, MIME boundaries, headers, converts HTML to text)
cleanEmailContent := imap.ExtractMessageContent(email.Body)
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"cleanSize": len(cleanEmailContent),
}).Debug("Extracted clean email content")
// Calculate token count for validation (use English as default to avoid API call)
// Language detection will happen during actual GenerateReply
systemPrompt := aiProcessor.BuildSystemPrompt("English")
userPrompt := aiProcessor.BuildUserPrompt(contextContent, cleanEmailContent)
estimatedTokens := tokenCounter.EstimateFullRequest(systemPrompt, userPrompt)
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"estimatedTokens": estimatedTokens,
"maxTokens": cfg.Processing.MaxTokens,
}).Debug("Calculated token estimate for email")
// Check token limit
if cfg.Processing.MaxTokens > 0 && estimatedTokens > cfg.Processing.MaxTokens {
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"from": email.From,
"estimatedTokens": estimatedTokens,
"maxTokens": cfg.Processing.MaxTokens,
}).Warn("Email exceeds token limit, marking as AI-processed but keeping in inbox")
skippedCount++
// Mark as AI-processed to prevent reprocessing, but DON'T move the email
if markErr := imapClient.MarkAsAIProcessed(email); markErr != nil {
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"error": markErr,
}).Error("Failed to mark oversized email as AI-processed")
} else {
logger.WithField("subject", email.Subject).Info("Marked oversized email as AI-processed (email remains in inbox)")
}
continue
}
// Generate AI response with clean content
response, err := aiProcessor.GenerateReply(cleanEmailContent, contextContent)
if err != nil {
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"error": err,
}).Error("Failed to generate reply")
errorCount++
// Mark as AI-processed even on failure to prevent infinite retry loop
if markErr := imapClient.MarkAsAIProcessed(email); markErr != nil {
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"error": markErr,
}).Error("Failed to mark failed email as AI-processed")
} else {
logger.WithField("subject", email.Subject).Info("Marked failed email as AI-processed to prevent reprocessing")
}
continue
}
logger.WithField("responseLength", len(response)).Debug("Generated AI response")
// Mark as AI-processed immediately to prevent reprocessing if subsequent steps fail
if err := imapClient.MarkAsAIProcessed(email); err != nil {
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"error": err,
}).Warn("Failed to mark email as AI-processed, continuing with draft save")
// Continue anyway - if this fails, worst case is we might reprocess
// But we want to try to save the draft we just generated
} else {
logger.Debug("Marked email as AI-processed")
}
// Save as draft
if err := imapClient.SaveDraft(email, response); err != nil {
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"error": err,
}).Error("Failed to save draft")
errorCount++
continue
}
logger.Debug("Saved response as draft")
// Mark email as processed
if err := imapClient.MarkAsProcessed(email); err != nil {
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"error": err,
}).Error("Failed to mark email as processed")
errorCount++
continue
}
processedCount++
logger.WithFields(logrus.Fields{
"subject": email.Subject,
"from": email.From,
}).Info("Successfully processed email")
}
logger.WithFields(logrus.Fields{
"totalEmails": len(emails),
"processed": processedCount,
"errors": errorCount,
"skipped": skippedCount,
}).Info("Completed email processing cycle")
}