emersion / go-imap-idle

IDLE extension for go-imap
MIT License
34 stars 12 forks source link

How to get updated message? #11

Closed webroru closed 4 years ago

webroru commented 5 years ago

Hello,

I ran code from example, sent mail to myself and got message in console: "New update: &{0xc000114000}". What is this uid? Is it possible to get a message by this number?

taisph commented 5 years ago

@webroru

Update can be one of multiple types (ExpungeUpdate, MessageUpdate,MailboxUpdate...) . You can use type assertion to find out which one. MailboxUpdate is the one I look for when expecting new mail, although for me it subsequently hangs when trying to fetch messages after receiving the update.

    switch v := update.(type) {
    case *client.MailboxUpdate:
        // Do something.
    default:
        log.Info("skipping update")
    }
bertabus-zz commented 5 years ago

I had to perform a type assertion on the interface. So after switching on the underlying type I've asserted to start working with the underlying types. Something like,

    // Listen for updates
    for {
        select {
        case update := <-updates:
            log.Printf("New update: %v", update)//This will print the pointer address
            log.Println(reflect.TypeOf(update).String())//Tell you what type you have
            switch update.(type){
            case *client.MessageUpdate:
                log.Println("Found message update type")
                msg, _ := update.(*client.MessageUpdate)//This prints what you want here
                log.Println(msg.Message)
            case *client.MailboxUpdate:
                log.Println("Found mailbox update type")
                mbx, _ := update.(*client.MailboxUpdate)
                log.Println(mbx.Mailbox)
                       //etc....
            }
            // if update
        case err := <-done:
            if err != nil {
                log.Fatal(err)
            }
            log.Println("Not idling anymore")
            return
        }
    }

It's great to be able to accept any type but I think it is safe to assume that most people want to work with the underlying type in these situations. It would have saved me a little time surfing through godoc if this had been part of the readme, maybe a pull should be added for the readme.

emersion commented 5 years ago

You don't need a type assertion. You can use @taisph's code snippet and v already has the right type. This is "just" Go syntax, see e.g. the last part of https://gobyexample.com/switch. I'm willing to merge a PR to add this to example_test.go (maybe ExampleClient_IdleWithFallback?).

Also note that it's never a good idea to do things like

msg, _ := update.(*client.MessageUpdate)

because it'll ignore errors. Instead, do

msg := update.(*client.MessageUpdate)

so that it panics if the assertion doesn't hold.

bertabus-zz commented 5 years ago

I completely overlooked the v := assignment and honestly I'm just getting back into Go coding after a couple years off and think I was bringing some bad python habits back with me. Updated my code and works as expected. Also noted on the panic, but I don't need to worry anymore in this case as the assertion is not necessary.

I am however now getting a similar issue as @taisph, I suspect I just have a blocking channel in my code somewhere but thought I would confirm the possible issue. It works if I call another routine that reconnects and creates a new client. I think I need some further analysis though before claiming it's an issue.

I do think a PR with a couple of these cases to illustrate the underlying types would be helpful for folks and will push an update when I am able to get a chance.

Oh, and thanks for the libraries :)

taisph commented 5 years ago

FWIW. I ended up with something like the code below. I only need the MailboxUpdate to get notified about new mail. It keeps listening for updates until an MailboxUpdate arrives and then exits IDLE freeing the client for fetching messages after which it will reenter the wait function.


func (app *App) Run() {
    // ...
    for {
        upd, err := app.waitForMailboxUpdate(chanupd)
        if err != nil {
            panic(err)
        }
        app.fetchMessages(upd.Mailbox)
    }
}

func (app *App) waitForMailboxUpdate(chupd chan client.Update) (*client.MailboxUpdate, error) {
    done := make(chan error, 1)
    stop := make(chan struct{})
    go func() {
        done <- app.imapIdleClient.IdleWithFallback(stop, 5*time.Minute)
    }()

    var mboxupd *client.MailboxUpdate
waitLoop:
    for {
        select {
        case upd := <-chupd:
            if mboxupd = asMailboxUpdate(upd); mboxupd != nil {
                break waitLoop
            }
        case err := <-done:
            if err != nil {
                return nil, fmt.Errorf("error while idling: %s", err.Error())
            }
            return nil, nil
        }
    }

    close(stop)
    <-done

    return mboxupd, nil
}

func asMailboxUpdate(upd client.Update) *client.MailboxUpdate {
    if v, ok := upd.(*client.MailboxUpdate); ok {
        return v
    }
    return nil
}
BRUHItsABunny commented 4 years ago

How would one stop idling in order to fetch a message and then start again? I have been trying to implement that on top of very similar code to this but I keep getting short read errors whenever I either send something into the stop channel or close the stop channel.

Actually when I send something into the stop channel I can fetch the message but if I start idling again it fails with short read error. When I close the stop channel I get an error even when fetching.

func (ec *EClient) ListenForEmails() {
    // Listen for updates
    for {
        select {
        case update := <-ec.IdleUpdates:
            switch update.(type) {
            case *client.MailboxUpdate:
                // ec.IdleStop <- struct {}{} // Fetches fine, short read error when trying to fetch again
                // close(ec.IdleStop) // short read error upon trying to fetch as well as when trying to re-idle
                realUpdate := update.(*client.MailboxUpdate)
                uid := realUpdate.Mailbox.Messages
                messages := make(chan *imap.Message, 10)
                seq := new(imap.SeqSet)
                seq.AddRange(uid, uid)
                err := ec.Client.Fetch(seq, []imap.FetchItem{imap.FetchBody + "[]"}, messages)
                if err == nil {
                    result := parseMails(messages)
                    for _, email := range result {
                        log.Println(email.Date, "\t", email.From, "\t", email.Subject, "\t", email.HTMLBody)
                    }
                } else {
                    // Deal with error
                    log.Println("Fetch error: ", err)
                }
            default:
                log.Println("New update: \n", spew.Sdump(update))
            }
        case err := <-ec.IdleErrors:
            if err != nil {
                log.Println(err)
            }
            log.Println("Not idling anymore")
            // re-idle
            ec.InitIdle()
        }
    }
}

InitIdle looks like this:

func (ec *EClient) InitIdle() {
    // Create IDLE client
    idleClient := idle.NewClient(ec.Client)
    // Create a channel to receive mailbox updates
    updates := make(chan client.Update)
    ec.Client.Updates = updates
    // Start idling
    idleErr := make(chan error, 1)
    idleStop:= make(chan struct{}, 1)
    go func() {
        idleErr <- idleClient.IdleWithFallback(idleStop, 0)
    }()
    ec.IdleErrors = idleErr
    ec.IdleUpdates = updates
    ec.IdleStop = idleStop
}
BRUHItsABunny commented 4 years ago

Nevermind, I had to reconnect all together after closing the stop channel and then re-idle after fetching the message.

xplodwild commented 4 years ago

Same issue hitting here, after closing the stop channel, the Idle() method keeps waiting infinitely on return <-done, thus blocking all calls on the IMAP client, which waits for the response of.. something.

andreasbenzing commented 4 years ago

I have the same problem as @xplodwild . Any updates on this?

emersion commented 4 years ago

Same issue hitting here, after closing the stop channel, the Idle() method keeps waiting infinitely on return <-done, thus blocking all calls on the IMAP client, which waits for the response of.. something.

Can you open a separate issue for this?

Closing this one since the original question was answered.

AnsonCode commented 4 years ago

FWIW. I ended up with something like the code below. I only need the MailboxUpdate to get notified about new mail. It keeps listening for updates until an MailboxUpdate arrives and then exits IDLE freeing the client for fetching messages after which it will reenter the wait function.

func (app *App) Run() {
  // ...
  for {
      upd, err := app.waitForMailboxUpdate(chanupd)
      if err != nil {
          panic(err)
      }
      app.fetchMessages(upd.Mailbox)
  }
}

func (app *App) waitForMailboxUpdate(chupd chan client.Update) (*client.MailboxUpdate, error) {
  done := make(chan error, 1)
  stop := make(chan struct{})
  go func() {
      done <- app.imapIdleClient.IdleWithFallback(stop, 5*time.Minute)
  }()

  var mboxupd *client.MailboxUpdate
waitLoop:
  for {
      select {
      case upd := <-chupd:
          if mboxupd = asMailboxUpdate(upd); mboxupd != nil {
              break waitLoop
          }
      case err := <-done:
          if err != nil {
              return nil, fmt.Errorf("error while idling: %s", err.Error())
          }
          return nil, nil
      }
  }

  close(stop)
  <-done

  return mboxupd, nil
}

func asMailboxUpdate(upd client.Update) *client.MailboxUpdate {
  if v, ok := upd.(*client.MailboxUpdate); ok {
      return v
  }
  return nil
}

but i do not work. I can not revice the email. I use QQMail.

AnsonCode commented 4 years ago
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/emersion/go-imap"
    idle "github.com/emersion/go-imap-idle"
    "github.com/emersion/go-imap/client"
)

const (
    addr     = "imap.qq.com:993"
    username = "xxx@qq.com"
    password = "xxx"
)

type App struct {
    imapIdleClient *idle.Client
}

func (app *App) Run() {
    if ok, err := app.imapIdleClient.SupportIdle(); err == nil && ok {
        log.Println("支持imap idle")
    } else {
        log.Println("不支持imap idle")
    }
    chanupd := make(chan client.Update)
    // ...
    for {
        upd, err := app.waitForMailboxUpdate(chanupd)
        if err != nil {
            panic(err)
        }
        app.fetchMessages(upd.Mailbox)
    }
}

func (app *App) waitForMailboxUpdate(chupd chan client.Update) (*client.MailboxUpdate, error) {
    done := make(chan error, 1)
    stop := make(chan struct{})
    go func() {
        done <- app.imapIdleClient.IdleWithFallback(stop, 5*time.Minute)
    }()

    var mboxupd *client.MailboxUpdate
waitLoop:
    for {
        select {
        case upd := <-chupd:
            if mboxupd = asMailboxUpdate(upd); mboxupd != nil {
                break waitLoop
            }
        case err := <-done:
            if err != nil {
                return nil, fmt.Errorf("error while idling: %s", err.Error())
            }
            return nil, nil
        }
    }

    close(stop)
    <-done

    return mboxupd, nil
}

func asMailboxUpdate(upd client.Update) *client.MailboxUpdate {
    if v, ok := upd.(*client.MailboxUpdate); ok {
        return v
    }
    return nil
}

func (this *App) fetchMessages(mailbox *imap.MailboxStatus) {
    log.Println("fetchMessages", mailbox)
}

func main() {
    c, err := client.DialTLS(addr, nil)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("Connected")

    // Don't forget to logout
    defer c.Logout()

    // Login
    if err := c.Login(username, password); err != nil {
        log.Fatal(err)
    }
    log.Println("Logged in")
    // Select INBOX
    mbox, err := c.Select("INBOX", false)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("Flags for INBOX:", mbox.Flags)
    idleClient := idle.NewClient(c)
    app := &App{idleClient}
    app.Run()
}