From d8dc7818e535a6620336ddb3cc2d257d2569791b Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Thu, 29 May 2025 16:13:47 +0200 Subject: [PATCH] feat: Enhance error handling and logging in SaveDraft and ensureFolder methods, adding recovery from panics and detailed debug information. --- internal/imap/imap.go | 218 +++++++++++++++++++++++++++++++++++------- 1 file changed, 182 insertions(+), 36 deletions(-) diff --git a/internal/imap/imap.go b/internal/imap/imap.go index 49e90c3..fa8be0e 100644 --- a/internal/imap/imap.go +++ b/internal/imap/imap.go @@ -205,7 +205,18 @@ func (ic *IMAPClient) FetchUnprocessedEmails() ([]Email, error) { return emails, nil } -func (ic *IMAPClient) SaveDraft(email Email, response string) error { +func (ic *IMAPClient) SaveDraft(email Email, response string) (err error) { + defer func() { + if r := recover(); r != nil { + logger.WithFields(logrus.Fields{ + "panic": r, + "emailSubject": email.Subject, + "draftBox": ic.config.DraftBox, + }).Error("Panic occurred in SaveDraft") + err = fmt.Errorf("panic in SaveDraft: %v", r) + } + }() + if err := ic.ensureConnection(); err != nil { return fmt.Errorf("failed to ensure IMAP connection: %v", err) } @@ -213,13 +224,16 @@ func (ic *IMAPClient) SaveDraft(email Email, response string) error { // Get proper folder path and ensure it exists draftFolder, err := ic.ensureFolder(ic.config.DraftBox) if err != nil { - return fmt.Errorf("failed to ensure draft folder exists: %v", err) + return fmt.Errorf("failed to ensure draft folder: %s, error: %v", ic.config.DraftBox, err) } + logger.WithField("draftFolder", draftFolder).Debug("Ensured draft folder path") + logger.WithField("draftFolder", draftFolder).Debug("Attempting to select draft folder") _, err = ic.client.Select(draftFolder, false) if err != nil { - return fmt.Errorf("failed to select draft box: %v", err) + return fmt.Errorf("failed to select draft box '%s': %v", draftFolder, err) } + logger.WithField("draftFolder", draftFolder).Debug("Successfully selected draft folder") // Format the draft message with HTML response and original email headers + content draft := fmt.Sprintf("From: %s\r\n"+ @@ -255,9 +269,14 @@ func (ic *IMAPClient) SaveDraft(email Email, response string) error { // Save the draft to the proper folder path flags := []string{"\\Draft"} + logger.WithFields(logrus.Fields{ + "draftFolder": draftFolder, + "flags": flags, + }).Debug("Attempting to append draft") if err := ic.client.Append(draftFolder, flags, time.Now(), literal); err != nil { - return fmt.Errorf("failed to append draft: %v", err) + return fmt.Errorf("failed to append draft to '%s': %v", draftFolder, err) } + logger.WithField("draftFolder", draftFolder).Debug("Successfully appended draft") return nil } @@ -419,44 +438,170 @@ func fallbackExtractContent(body string) string { } // ensureFolder makes sure a folder exists and returns its full path using proper delimiters -func (ic *IMAPClient) ensureFolder(folderName string) (string, error) { - // List all mailboxes to get the delimiter - mailboxes := make(chan *imap.MailboxInfo, 10) - done := make(chan error, 1) - go func() { - done <- ic.client.List("", "*", mailboxes) +func (ic *IMAPClient) ensureFolder(folderName string) (path string, err error) { + defer func() { + if r := recover(); r != nil { + logger.WithFields(logrus.Fields{ + "panic": r, + "folderName": folderName, + }).Error("Panic occurred in ensureFolder") + err = fmt.Errorf("panic in ensureFolder: %v", r) + } }() + logger.WithField("folderNameInput", folderName).Debug("Ensuring folder exists (ensureFolder start)") - var delimiter string - for m := range mailboxes { - delimiter = m.Delimiter - break // We just need the first one to get the delimiter - } + var folderPath string + isGmail := strings.ToLower(ic.config.Server) == "imap.gmail.com" - if err := <-done; err != nil { - return "", fmt.Errorf("failed to list mailboxes: %v", err) - } - - if delimiter == "" { - delimiter = "/" // fallback to common delimiter - } - - // Replace any forward slashes with the server's delimiter - folderPath := strings.ReplaceAll(folderName, "/", delimiter) - - // Try to create the folder if it doesn't exist - if err := ic.client.Create(folderPath); err != nil { - // Ignore errors if the folder already exists + if isGmail { + logger.Debug("ensureFolder: Detected Gmail server. Using Gmail-specific logic.") + // Gmail always uses '/' as a delimiter and folder names are typically as provided. + delimiter := "/" + folderPath = folderName // Gmail folder names in config should already be correct. logger.WithFields(logrus.Fields{ - "folder": folderPath, - "error": err, - }).Debug("Folder creation failed (might already exist)") - } + "folderName": folderName, + "assumedDelimiter": delimiter, + "derivedFolderPath": folderPath, + }).Debug("ensureFolder: Gmail - folder path set") - return folderPath, nil + // For Gmail, don't try to CREATE system folders like "[Gmail]/..." or "INBOX" + // For "INBOX/Subfolder", CREATE is fine as it creates a label. + if strings.HasPrefix(folderPath, "[Gmail]/") || folderPath == "INBOX" { + logger.WithField("folderPath", folderPath).Debug("ensureFolder: Gmail - Skipping CREATE for system folder.") + return folderPath, nil + } + // For other paths like "INBOX/Done" or "MyCustomLabel", attempt CREATE. + logger.WithField("folderPath", folderPath).Debug("ensureFolder: Gmail - Attempting to create folder (label).") + if createErr := ic.client.Create(folderPath); createErr != nil { + if !strings.Contains(strings.ToLower(createErr.Error()), "already exists") && + !strings.Contains(strings.ToLower(createErr.Error()), "mailbox exists") { + logger.WithFields(logrus.Fields{ + "folder": folderPath, + "error": createErr, + }).Warn("ensureFolder: Gmail - Folder creation failed (and not 'already exists')") + // Unlike non-Gmail, if CREATE fails for a non-system folder, it's more likely a real issue. + // However, subsequent SELECT/APPEND will ultimately determine usability. + // For now, we'll proceed and let later operations fail if it's critical. + } else { + logger.WithFields(logrus.Fields{ + "folder": folderPath, + "error": createErr, + }).Debug("ensureFolder: Gmail - Folder creation failed (likely already exists).") + } + } else { + logger.WithField("folderPath", folderPath).Info("ensureFolder: Gmail - Successfully created folder/label or it already existed.") + } + return folderPath, nil + + } else { + logger.Debug("ensureFolder: Non-Gmail server. Using generic logic.") + // Generic logic for non-Gmail servers (existing logic with timeouts) + logger.Debug("ensureFolder: Creating channels mailboxes and listDone") + mailboxes := make(chan *imap.MailboxInfo, 10) + listDone := make(chan error, 1) + logger.Debug("ensureFolder: Channels created. Attempting to start List goroutine") + + go func() { + logger.Debug("ensureFolder: List goroutine started") + defer func() { + if r := recover(); r != nil { + logger.WithFields(logrus.Fields{ + "panic": r, + "folderName": folderName, + }).Error("Panic occurred in ensureFolder's List goroutine") + listDone <- fmt.Errorf("panic in List goroutine: %v", r) + } + }() + logger.Debug("ensureFolder: List goroutine: Entering select for client.List or timeout") + select { + case listDone <- ic.client.List("", "*", mailboxes): + logger.Debug("ensureFolder: List goroutine: client.List call completed and sent to listDone") + case <-time.After(30 * time.Second): + logger.Error("Timeout occurred during client.List operation in goroutine") + listDone <- fmt.Errorf("client.List timeout after 30 seconds in goroutine") + } + logger.Debug("ensureFolder: List goroutine finished") + }() + logger.Debug("ensureFolder: List goroutine launched.") + + var delimiter string + var receivedMailbox bool + logger.Debug("ensureFolder: Attempting to range over mailboxes channel") + for m := range mailboxes { + receivedMailbox = true + logger.WithField("mailboxName", m.Name).Debug("ensureFolder: Received mailbox from channel") + delimiter = m.Delimiter + logger.WithField("delimiter", delimiter).Debug("ensureFolder: Got delimiter from mailbox info") + break + } + if !receivedMailbox { + logger.Debug("ensureFolder: Did not receive any mailboxes from channel (it might have been empty or closed early).") + } + logger.Debug("ensureFolder: Finished ranging over mailboxes. Attempting to read from listDone channel with timeout") + + select { + case listErr := <-listDone: + if listErr != nil { + logger.WithError(listErr).Error("ensureFolder: Error received from listDone channel") + return "", fmt.Errorf("failed to list mailboxes to determine delimiter: %v", listErr) + } + logger.Debug("ensureFolder: Successfully read from listDone channel.") + case <-time.After(35 * time.Second): + logger.Error("ensureFolder: Timeout waiting for listDone channel") + return "", fmt.Errorf("timeout waiting for LIST operation to complete") + } + + if delimiter == "" { + logger.Debug("ensureFolder: Delimiter is still empty after processing listDone and mailboxes.") + delimiter = "/" + logger.Debug("No delimiter returned by server, using fallback '/'") + } + logger.WithField("delimiter", delimiter).Debug("Determined mailbox delimiter") + + if delimiter != "/" { + folderPath = strings.ReplaceAll(folderName, "/", delimiter) + } else { + folderPath = folderName + } + logger.WithFields(logrus.Fields{ + "originalFolderName": folderName, + "serverDelimiter": delimiter, + "derivedFolderPath": folderPath, + }).Debug("Derived folder path using server delimiter") + + logger.WithField("folderPath", folderPath).Debug("Attempting to create folder if it doesn't exist") + if createErr := ic.client.Create(folderPath); createErr != nil { + if !strings.Contains(strings.ToLower(createErr.Error()), "already exists") && + !strings.Contains(strings.ToLower(createErr.Error()), "mailbox exists") { + logger.WithFields(logrus.Fields{ + "folder": folderPath, + "error": createErr, + }).Warn("Folder creation failed (and not 'already exists')") + } else { + logger.WithFields(logrus.Fields{ + "folder": folderPath, + "error": createErr, + }).Debug("Folder creation failed (likely already exists)") + } + } else { + logger.WithField("folderPath", folderPath).Info("Successfully created folder or it already existed") + } + return folderPath, nil + } } -func (ic *IMAPClient) MarkAsProcessed(email Email) error { +func (ic *IMAPClient) MarkAsProcessed(email Email) (err error) { + defer func() { + if r := recover(); r != nil { + logger.WithFields(logrus.Fields{ + "panic": r, + "emailSubject": email.Subject, + "processedBox": ic.config.ProcessedBox, + }).Error("Panic occurred in MarkAsProcessed") + err = fmt.Errorf("panic in MarkAsProcessed: %v", r) + } + }() + if err := ic.ensureConnection(); err != nil { return fmt.Errorf("failed to ensure IMAP connection: %v", err) } @@ -468,8 +613,9 @@ func (ic *IMAPClient) MarkAsProcessed(email Email) error { // Get proper folder path and ensure it exists processedFolder, err := ic.ensureFolder(ic.config.ProcessedBox) if err != nil { - return fmt.Errorf("failed to ensure processed folder exists: %v", err) + return fmt.Errorf("failed to ensure processed folder: %s, error: %v", ic.config.ProcessedBox, err) } + logger.WithField("processedFolder", processedFolder).Debug("Ensured processed folder path") // Select source mailbox _, err = ic.client.Select(ic.config.MailboxIn, false)