diff --git a/.gitignore b/.gitignore index 43d543e..187669c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ config.yaml # Binary -paraclub-ai-mailer +./paraclub-ai-mailer # Log files *.log @@ -13,4 +13,4 @@ paraclub-ai-mailer # OS specific files .DS_Store -*~ \ No newline at end of file +*~ diff --git a/cmd/paraclub-ai-mailer/main.go b/cmd/paraclub-ai-mailer/main.go new file mode 100644 index 0000000..1a28588 --- /dev/null +++ b/cmd/paraclub-ai-mailer/main.go @@ -0,0 +1,172 @@ +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 int + for _, email := range emails { + logger.WithFields(logrus.Fields{ + "subject": email.Subject, + "from": email.From, + "messageId": email.ID, + }).Info("Processing email") + + // Generate AI response + response, err := aiProcessor.GenerateReply(email.Body, contextContent) + if err != nil { + logger.WithFields(logrus.Fields{ + "subject": email.Subject, + "error": err, + }).Error("Failed to generate reply") + errorCount++ + continue + } + logger.WithField("responseLength", len(response)).Debug("Generated AI response") + + // 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, + }).Info("Completed email processing cycle") +}