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" "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) 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, 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, 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 { emailBodySize := len(email.Body) logger.WithFields(logrus.Fields{ "subject": email.Subject, "from": email.From, "messageId": email.ID, "bodySizeBytes": emailBodySize, }).Info("Processing email") // Check email size limit if cfg.Processing.MaxEmailSizeBytes > 0 && emailBodySize > cfg.Processing.MaxEmailSizeBytes { logger.WithFields(logrus.Fields{ "subject": email.Subject, "from": email.From, "bodySizeBytes": emailBodySize, "maxSizeBytes": cfg.Processing.MaxEmailSizeBytes, }).Warn("Email body exceeds size limit, skipping") skippedCount++ // Mark as AI-processed to prevent reprocessing 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") } continue } // Extract clean email content (remove MIME boundaries, headers, etc.) cleanEmailContent := imap.ExtractMessageContent(email.Body) cleanContentSize := len(cleanEmailContent) logger.WithFields(logrus.Fields{ "subject": email.Subject, "rawSize": emailBodySize, "cleanSize": cleanContentSize, "sizeReduction": emailBodySize - cleanContentSize, }).Debug("Extracted clean email content") // 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") }