diff --git a/README.md b/README.md
index 0805370..6370013 100644
--- a/README.md
+++ b/README.md
@@ -511,6 +511,23 @@ Connect to IRC networks:
```
+
+Email
+
+```json
+{
+ "smtpServer": "smtp.gmail.com:587",
+ "imapServer": "imap.gmail.com:993",
+ "smtpInsecure": "false",
+ "imapInsecure": "false",
+ "username": "user@gmail.com",
+ "email": "user@gmail.com",
+ "password": "correct-horse-battery-staple",
+ "name": "LogalAGI Agent"
+}
+```
+
+
## REST API
diff --git a/go.mod b/go.mod
index 02c1206..4497961 100644
--- a/go.mod
+++ b/go.mod
@@ -32,7 +32,10 @@ require (
mvdan.cc/xurls/v2 v2.6.0
)
+require github.com/JohannesKaufmann/dom v0.2.0 // indirect
+
require (
+ github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.2
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
@@ -45,6 +48,10 @@ require (
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
+ github.com/emersion/go-imap/v2 v2.0.0-beta.5 // indirect
+ github.com/emersion/go-message v0.18.2 // indirect
+ github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
+ github.com/emersion/go-smtp v0.22.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
@@ -60,6 +67,7 @@ require (
github.com/gofiber/utils v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
+ github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
diff --git a/go.sum b/go.sum
index 5cb36cb..26cfd04 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,7 @@
+github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
+github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
+github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.2 h1:eeMLttqTjTgILD6no79Ge96V7Wv8pWDfMVn4jy+koIY=
+github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.2/go.mod h1:HtsP+1Fchp4dVvaiIsLHAl/yqL3H1YLwqLC9kNwqQEg=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
@@ -37,6 +41,19 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
github.com/donseba/go-htmx v1.12.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
+github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
+github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
+github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA=
+github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
+github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
+github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
+github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
+github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-smtp v0.22.0 h1:/d3HWxkZZ4riB+0kzfoODh9X+xyCrLEezMnAAa1LEMU=
+github.com/emersion/go-smtp v0.22.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
@@ -83,6 +100,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
+github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
diff --git a/services/connectors.go b/services/connectors.go
index db9b262..1a5ddd8 100644
--- a/services/connectors.go
+++ b/services/connectors.go
@@ -20,6 +20,7 @@ const (
ConnectorGithubPRs = "github-prs"
ConnectorTwitter = "twitter"
ConnectorMatrix = "matrix"
+ ConnectorEmail = "email"
)
var AvailableConnectors = []string{
@@ -31,6 +32,7 @@ var AvailableConnectors = []string{
ConnectorGithubPRs,
ConnectorTwitter,
ConnectorMatrix,
+ ConnectorEmail,
}
func Connectors(a *state.AgentConfig) []state.Connector {
@@ -70,6 +72,8 @@ func Connectors(a *state.AgentConfig) []state.Connector {
conns = append(conns, cc)
case ConnectorMatrix:
conns = append(conns, connectors.NewMatrix(config))
+ case ConnectorEmail:
+ conns = append(conns, connectors.NewEmail(config))
}
}
return conns
@@ -117,5 +121,10 @@ func ConnectorsConfigMeta() []config.FieldGroup {
Label: "Matrix",
Fields: connectors.MatrixConfigMeta(),
},
+ {
+ Name: "email",
+ Label: "Email",
+ Fields: connectors.EmailConfigMeta(),
+ },
}
}
diff --git a/services/connectors/email.go b/services/connectors/email.go
new file mode 100644
index 0000000..711b530
--- /dev/null
+++ b/services/connectors/email.go
@@ -0,0 +1,424 @@
+package connectors
+
+import (
+ "bytes"
+ "fmt"
+ "mime"
+ "strings"
+ "time"
+
+ htmltomarkdown "github.com/JohannesKaufmann/html-to-markdown/v2"
+ imap "github.com/emersion/go-imap/v2"
+ sasl "github.com/emersion/go-sasl"
+ smtp "github.com/emersion/go-smtp"
+
+ "github.com/emersion/go-imap/v2/imapclient"
+ "github.com/emersion/go-message"
+ "github.com/emersion/go-message/charset"
+ "github.com/gomarkdown/markdown"
+ "github.com/gomarkdown/markdown/html"
+ "github.com/gomarkdown/markdown/parser"
+
+ "github.com/mudler/LocalAGI/core/agent"
+ "github.com/mudler/LocalAGI/core/types"
+ "github.com/mudler/LocalAGI/pkg/config"
+ "github.com/mudler/LocalAGI/pkg/xlog"
+ "github.com/sashabaranov/go-openai"
+)
+
+type Email struct {
+ username string
+ name string
+ password string
+ email string
+ smtpServer string
+ smtpInsecure bool
+ imapServer string
+ imapInsecure bool
+}
+
+func NewEmail(config map[string]string) *Email {
+
+ return &Email{
+ username: config["username"],
+ name: config["name"],
+ password: config["password"],
+ email: config["email"],
+ smtpServer: config["smtpServer"],
+ smtpInsecure: config["smtpInsecure"] == "true",
+ imapServer: config["imapServer"],
+ imapInsecure: config["imapInsecure"] == "true",
+ }
+}
+
+func EmailConfigMeta() []config.Field {
+ return []config.Field{
+ {
+ Name: "smtpServer",
+ Label: "SMTP Host:port",
+ Type: config.FieldTypeText,
+ Required: true,
+ HelpText: "SMTP server host:port (e.g., smtp.gmail.com:587)",
+ },
+ {
+ Name: "smtpInsecure",
+ Label: "Insecure SMTP",
+ Type: config.FieldTypeCheckbox,
+ },
+ {
+ Name: "imapServer",
+ Label: "IMAP Host:port",
+ Type: config.FieldTypeText,
+ Required: true,
+ HelpText: "IMAP server host:port (e.g., imap.gmail.com:993)",
+ },
+ {
+ Name: "imapInsecure",
+ Label: "Insecure IMAP",
+ Type: config.FieldTypeCheckbox,
+ },
+ {
+ Name: "username",
+ Label: "Username",
+ Type: config.FieldTypeText,
+ Required: true,
+ HelpText: "Username/email address",
+ },
+ {
+ Name: "name",
+ Label: "Friendly Name",
+ Type: config.FieldTypeText,
+ Required: true,
+ HelpText: "Friendly name of sender",
+ },
+ {
+ Name: "password",
+ Label: "Password",
+ Type: config.FieldTypeText,
+ Required: true,
+ HelpText: "SMTP/IMAP password or app password",
+ },
+ {
+ Name: "email",
+ Label: "From Email",
+ Type: config.FieldTypeText,
+ Required: true,
+ HelpText: "Agent email address",
+ },
+ }
+}
+
+func (e *Email) AgentResultCallback() func(state types.ActionState) {
+ return func(state types.ActionState) {
+ // Send the result to the bot
+ }
+}
+
+func (e *Email) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
+ return func(state types.ActionCurrentState) bool {
+ // Send the reasoning to the bot
+ return true
+ }
+}
+
+func filterEmailRecipients(input string, emailToRemove string) string {
+
+ addresses := strings.Split(strings.TrimPrefix(input, "To: "), ",")
+
+ var filtered []string
+ for _, address := range addresses {
+ address = strings.TrimSpace(address)
+ if !strings.Contains(address, emailToRemove) {
+ filtered = append(filtered, address)
+ }
+ }
+
+ if len(filtered) > 0 {
+ return strings.Join(filtered, ", ")
+ }
+ return ""
+}
+
+func (e *Email) sendMail(to, subject, content, replyToID, references string, emails []string, html bool) {
+
+ auth := sasl.NewPlainClient("", e.username, e.password)
+
+ contentType := "text/plain"
+ if html {
+ contentType = "text/html"
+ }
+
+ var replyHeaders string
+ if replyToID != "" {
+ referenceLine := strings.ReplaceAll(references+" "+replyToID, "\n", "")
+ replyHeaders = fmt.Sprintf("In-Reply-To: %s\r\nReferences: %s\r\n", replyToID, referenceLine)
+ }
+
+ // Build full message content
+ var builder strings.Builder
+ fmt.Fprintf(&builder, "To: %s\r\n", to)
+ fmt.Fprintf(&builder, "From: %s <%s>\r\n", e.name, e.email)
+ builder.WriteString(replyHeaders)
+ fmt.Fprintf(&builder, "MIME-Version: 1.0\r\nContent-Type: %s;\r\n", contentType)
+ fmt.Fprintf(&builder, "Subject: %s\r\n\r\n", subject)
+ fmt.Fprintf(&builder, "%s\r\n", content)
+ msg := strings.NewReader(builder.String())
+
+ if !e.smtpInsecure {
+
+ err := smtp.SendMail(e.smtpServer, auth, e.email, emails, msg)
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email send err: %v", err))
+ }
+
+ } else {
+
+ c, err := smtp.Dial(e.smtpServer)
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email connection err: %v", err))
+ }
+ defer c.Close()
+
+ err = c.Hello("client")
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email hello err: %v", err))
+ }
+
+ err = c.Auth(auth)
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email auth err: %v", err))
+ }
+
+ err = c.SendMail(e.email, emails, msg)
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email send err: %v", err))
+ }
+
+ }
+}
+
+func imapWorker(done chan bool, e *Email, a *agent.Agent, c *imapclient.Client, startIndex uint32) {
+
+ currentIndex := startIndex
+
+ for {
+ select {
+ case <-done:
+
+ xlog.Info("Stopping imapWorker")
+ err := c.Logout().Wait()
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email IMAP logout fail: %v", err))
+ }
+ return
+
+ default:
+
+ selectedMbox, err := c.Select("INBOX", nil).Wait()
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email IMAP mailbox err: %v", err))
+ }
+
+ // Loop over any new messages recieved in selected mailbox
+ for currentIndex < selectedMbox.NumMessages {
+
+ currentIndex++
+
+ // Download email info
+ seqSet := imap.SeqSetNum(currentIndex)
+ bodySection := &imap.FetchItemBodySection{}
+ fetchOptions := &imap.FetchOptions{
+ Flags: true,
+ Envelope: true,
+ BodySection: []*imap.FetchItemBodySection{bodySection},
+ }
+ messageBuffers, err := c.Fetch(seqSet, fetchOptions).Collect()
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email IMAP fetch err: %v", err))
+ }
+
+ // Start conversation goroutine
+ go func(e *Email, a *agent.Agent, c *imapclient.Client, fmb *imapclient.FetchMessageBuffer) {
+
+ // Download Email contents
+ r := bytes.NewReader(fmb.FindBodySection(bodySection))
+ msg, err := message.Read(r)
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email reader err: %v", err))
+ }
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(msg.Body)
+
+ xlog.Debug("New email!")
+ xlog.Debug(fmt.Sprintf("From: %s", msg.Header.Get("From")))
+ xlog.Debug(fmt.Sprintf("To: %s", msg.Header.Get("To")))
+ xlog.Debug(fmt.Sprintf("Subject: %s", msg.Header.Get("Subject")))
+
+ // In the event that an email account has multiple email addresses, only respond to the one configured
+ if !strings.Contains(msg.Header.Get("To"), e.email) {
+ xlog.Info(fmt.Sprintf("Email was sent to %s, but appeared in my inbox (%s). Ignoring!", msg.Header.Get("To"), e.email))
+ return
+ }
+
+ content := buf.String()
+ contentIsHTML := false
+
+ // Convert email to markdown only if it's in HTML
+ prefixes := []string{" md err: %v", err))
+ contentIsHTML = false
+ content = buf.String()
+ }
+ }
+ }
+
+ xlog.Debug(fmt.Sprintf("Markdown:\n\n%s", content))
+
+ // Construct prompt
+ prompt := fmt.Sprintf("%s %s:\n\nFrom: %s\nTime: %s\nSubject: %s\n=====\n%s",
+ "This email thread was sent to you. You are",
+ e.email,
+ msg.Header.Get("From"),
+ fmb.Envelope.Date.Format(time.RFC3339),
+ fmb.Envelope.Subject,
+ content,
+ )
+ conv := []openai.ChatCompletionMessage{}
+ conv = append(conv, openai.ChatCompletionMessage{Role: "user", Content: prompt})
+
+ // Send prompt to agent and wait for result
+ xlog.Debug(fmt.Sprintf("Starting conversation:\n\n%v", conv))
+ jobResult := a.Ask(types.WithConversationHistory(conv))
+ if jobResult.Error != nil {
+ xlog.Error(fmt.Sprintf("Error asking agent: %v", jobResult.Error))
+ }
+
+ // Send agent response to user, replying to original email.
+ xlog.Debug("Agent finished responding. Sending reply email to user")
+
+ // Get a list of emails to respond to ("Reply All" logic)
+ // This could be done through regex, but it's probably safer to rebuild explicitly
+ fromEmail := fmt.Sprintf("%s@%s", fmb.Envelope.From[0].Mailbox, fmb.Envelope.From[0].Host)
+ emails := []string{}
+ emails = append(emails, fromEmail)
+
+ for _, addr := range fmb.Envelope.To {
+ if addr.Mailbox != "" && addr.Host != "" {
+ email := fmt.Sprintf("%s@%s", addr.Mailbox, addr.Host)
+ if email != e.email {
+ emails = append(emails, email)
+ }
+ }
+ }
+
+ // Keep the original header, in case sender had contact names as part of the header
+ newToHeader := msg.Header.Get("From") + ", " + filterEmailRecipients(msg.Header.Get("To"), e.email)
+
+ // Create the body of the email
+ replyContent := jobResult.Response
+ if jobResult.Response == "" {
+ replyContent =
+ "System: I'm sorry, but it looks like the agent did not respond. " +
+ "This could be in error, or maybe it had nothing to say."
+ }
+
+ // Quote the original message. This lets the agent see conversation history and is an email standard.
+ quoteHeader := fmt.Sprintf("\r\n\r\nOn %s, %s wrote:\n",
+ fmb.Envelope.Date.Format("Monday, Jan 2, 2006 at 15:04"),
+ fmt.Sprintf("%s <%s>", fmb.Envelope.From[0].Name, fromEmail),
+ )
+ quotedLines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
+ for i, line := range quotedLines {
+ quotedLines[i] = "> " + line
+ }
+ replyContent = replyContent + quoteHeader + strings.Join(quotedLines, "\r\n")
+
+ // If the original email was sent in HTML, reply with HTML
+ if contentIsHTML {
+ p := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock)
+ doc := p.Parse([]byte(replyContent))
+
+ opts := html.RendererOptions{Flags: html.CommonFlags | html.HrefTargetBlank | html.CompletePage}
+ renderer := html.NewRenderer(opts)
+
+ replyContent = string(markdown.Render(doc, renderer))
+ }
+
+ // Send the email
+ e.sendMail(newToHeader,
+ fmt.Sprintf("Re: %s", msg.Header.Get("Subject")),
+ replyContent,
+ msg.Header.Get("Message-ID"),
+ msg.Header.Get("References"),
+ emails,
+ contentIsHTML,
+ )
+ }(e, a, c, messageBuffers[0])
+ }
+ time.Sleep(5 * time.Second) // Refresh inbox every n seconds
+ }
+ }
+}
+
+func (e *Email) Start(a *agent.Agent) {
+ go func() {
+
+ xlog.Info("Email connector is now running. Press CTRL-C to exit.")
+ // IMAP dial
+ imapOpts := &imapclient.Options{WordDecoder: &mime.WordDecoder{CharsetReader: charset.Reader}}
+ var c *imapclient.Client
+ var err error
+ if e.imapInsecure {
+ c, err = imapclient.DialInsecure(e.imapServer, imapOpts)
+ } else {
+ c, err = imapclient.DialTLS(e.imapServer, imapOpts)
+ }
+
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email IMAP dial err: %v", err))
+ return
+ }
+ defer c.Close()
+
+ // IMAP login
+ err = c.Login(e.username, e.password).Wait()
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email IMAP login err: %v", err))
+ return
+ }
+
+ // IMAP mailbox
+ mailboxes, err := c.List("", "%", nil).Collect()
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Email IMAP mailbox err: %v", err))
+ return
+ }
+
+ xlog.Debug(fmt.Sprintf("Email IMAP mailbox count: %v", len(mailboxes)))
+ for _, mbox := range mailboxes {
+ xlog.Debug(fmt.Sprintf(" - %v", mbox.Mailbox))
+ }
+
+ // Select INBOX
+ selectedMbox, err := c.Select("INBOX", nil).Wait()
+ if err != nil {
+ xlog.Error(fmt.Sprintf("Cannot select INBOX mailbox! %v", err))
+ return
+ }
+ xlog.Debug(fmt.Sprintf("INBOX contains %v messages", selectedMbox.NumMessages))
+
+ // Start checking INBOX for new mail
+ imapWorkerHandle := make(chan bool)
+ go imapWorker(imapWorkerHandle, e, a, c, selectedMbox.NumMessages)
+
+ <-a.Context().Done()
+ imapWorkerHandle <- true
+ xlog.Info("Email connector is now stopped.")
+
+ }()
+}