Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idle client v2 stops receiving new emails in some time. #602

Closed
serhiynovos opened this issue Mar 22, 2024 · 5 comments
Closed

Idle client v2 stops receiving new emails in some time. #602

serhiynovos opened this issue Mar 22, 2024 · 5 comments

Comments

@serhiynovos
Copy link

serhiynovos commented Mar 22, 2024

I need a service to listening for incoming emails and send them to internal server. It's working but I noticed that in some time like 1 2 days it stops receiving a new emails. In console I don't see any errors. Is there are any way I can check idle client status and if it's failed I can restart client or even just fail client so container with it will be restarted automatically ?

package client

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"sync"
	"text/template"

	"github.com/emersion/go-imap/v2"
	"github.com/emersion/go-imap/v2/imapclient"
	"github.com/mnako/letters"
	"mycom.imap.client/pkg/config"
	"mycom.imap.client/pkg/models"
	"mycom.imap.client/pkg/parsers"
	mycomserver "mycom.imap.client/pkg/mycom-server"
)

type IMAPClient struct {
	idleClient *imapclient.Client
	ctx        context.Context
	wg         *sync.WaitGroup
	idleCmd    *imapclient.IdleCommand
	config     config.Listener
}

func (client *IMAPClient) Start() {
	fmt.Printf("Start %s listener\n", client.config.Name)
	client.wg.Add(1)

	idleC, err := imapclient.DialTLS(client.config.Imap.Host, &imapclient.Options{
		UnilateralDataHandler: &imapclient.UnilateralDataHandler{
			Expunge: func(seqNum uint32) {
				log.Printf("message %v has been expunged", seqNum)
			},
			Mailbox: func(data *imapclient.UnilateralDataMailbox) {
				if data.NumMessages != nil {
					client.fetch(*data.NumMessages)
				}
			},
		},
	})

	if err != nil {
		log.Fatalf("failed to dial IMAP server: %v", err)
	}

	client.idleClient = idleC

	if err := idleC.Login(client.config.Imap.Username, client.config.Imap.Password).Wait(); err != nil {
		log.Fatalf("failed to login: %v", err)
	}

	if _, err := idleC.Select("INBOX", nil).Wait(); err != nil {
		log.Fatalf("failed to select INBOX: %v", err)
	}

	// Start idling
	idleCmd, err := idleC.Idle()
	if err != nil {
		log.Fatalf("IDLE command failed: %v", err)
	}
	client.idleCmd = idleCmd

	<-client.ctx.Done()
	client.Close()
}

func (client *IMAPClient) fetch(number uint32) {
	fmt.Println("Fetch", number)
	seqSet := imap.SeqSetNum(number)
	fetchOptions := &imap.FetchOptions{
		UID:         true,
		BodySection: []*imap.FetchItemBodySection{{}},
	}

	// Create a new fetch client for each fetch operation
	fetchC, err := imapclient.DialTLS(client.config.Imap.Host, nil)
	if err != nil {
		log.Fatalf("failed to dial IMAP server: %v", err)
	}
	defer fetchC.Close() // Ensure the client is closed after the operation

	if err := fetchC.Login(client.config.Imap.Username, client.config.Imap.Password).Wait(); err != nil {
		log.Fatalf("failed to login: %v", err)
	}

	if _, err := fetchC.Select("INBOX", nil).Wait(); err != nil {
		log.Fatalf("failed to select INBOX: %v", err)
	}

	fetchCmd := fetchC.Fetch(seqSet, fetchOptions)
	defer fetchCmd.Close()

	for {
		msg := fetchCmd.Next()
		if msg == nil {
			break
		}

		for {
			item := msg.Next()
			if item == nil {
				break
			}

			switch item := item.(type) {
			case imapclient.FetchItemDataUID:
				log.Printf("UID: %v", item.UID)
			case imapclient.FetchItemDataBodySection:
				email, err := letters.ParseEmail(item.Literal)
				if err != nil {
					log.Fatal(err)
				}

				from := email.Headers.From[0].Address
				subject := email.Headers.Subject
				message := email.Text

				fmt.Println("=========================================================================")

				fmt.Println("Sender ::", from)
				fmt.Println("Subject :: ", subject)
				fmt.Println("Email Text ::", message)

				canAccept := false

				if len(client.config.AcceptFrom) == 0 {
					canAccept = true
				} else {
					for _, acceptFrom := range client.config.AcceptFrom {
						if from == acceptFrom {
							canAccept = true
							break
						}
					}
				}

				if !canAccept {
					fmt.Println("Email is not in whitelist. Ignore this email ::", from)
					return
				}

				// Load the template
				tmpl, err := template.ParseFiles("mmmessage_template.txt")
				if err != nil {
					log.Fatalf("Error loading template: %v", err)
				}

				text, recipients := parsers.ParseIncomingEmailBody(message)

				if len(recipients) == 0 {
					fmt.Println("No recipients found. Ignore")
					return
				}

				sessionId, err := mycomserver.GetSessionID(client.config.Actions[0])

				if err != nil {
					fmt.Println("Failed to get session id", err.Error())
					return
				}

				// Define the data for the template
				data := models.MMMessageData{
					SessionID:    sessionId,
					EmailSubject: subject,
					Application:  "EmailListener",
					Priority:     "10",
					EmailText:    text,
					Recipients:   recipients,
				}

				// Execute the template with the data and save to a string variable
				var buf bytes.Buffer
				if err := tmpl.Execute(&buf, data); err != nil {
					log.Fatalf("Error executing template: %v", err)
				}
				renderedTemplate := buf.String()

				fmt.Println("Received template", renderedTemplate)

				if err := mycomserver.SendMMMessage(client.config.Actions[0], renderedTemplate); err != nil {
					fmt.Println("Failed sending MMMessage", err.Error())
				} else {
					fmt.Println("MMMessage sucessfully sent")
				}
			}
		}
	}
}

func (client *IMAPClient) Close() {
	log.Println("Close Client")
	client.idleClient.Close()
	client.idleCmd.Close()
	client.wg.Done()
}

func NewIMAPClient(ctx context.Context, wg *sync.WaitGroup, config config.Listener) *IMAPClient {
	return &IMAPClient{
		ctx:    ctx,
		wg:     wg,
		config: config,
	}
}

@emersion
Copy link
Owner

Since ca0ddb7, IdleCommand.Wait can be called before Close. It should now be possible to use it to figure out when the connection is broken.

@birudeghi
Copy link

@emersion Should we also include timeout adjustability regardless, as they won't be able to act on that new information without it?

@emersion
Copy link
Owner

emersion commented Apr 8, 2024

A connection can be closed by the server for many reasons, e.g. the server is restarted, or connectivity is lost for a moment.

@serhiynovos
Copy link
Author

@emersion is there any way to handle it ?

@emersion
Copy link
Owner

In your code snippet, you can add something like

if err := idleCmd.Wait(); err != nil {
    log.Fatalf("IDLE command failed: %v", err)
}

maybe in a goroutine since you're already blocking with <-client.ctx.Done().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants