feat: Enhance error handling and logging in SaveDraft and ensureFolder methods, adding recovery from panics and detailed debug information.

This commit is contained in:
2025-05-29 16:13:47 +02:00
parent 8903aa072a
commit d8dc7818e5

View File

@@ -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)