Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion imapclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,10 @@ func (c *Client) readResponseData(typ string) error {
}
case "NOMODSEQ":
// ignore
case "NOTIFICATIONOVERFLOW":
if c.options.UnilateralDataHandler.NotificationOverflow != nil {
c.options.UnilateralDataHandler.NotificationOverflow()
}
default: // [SP 1*<any TEXT-CHAR except "]">]
if c.dec.SP() {
c.dec.DiscardUntilByte(']')
Expand Down Expand Up @@ -1179,14 +1183,29 @@ type UnilateralDataMailbox struct {
//
// The handler will be invoked in an arbitrary goroutine.
//
// These handlers are important when using the IDLE or NOTIFY commands, as the
// server will send unsolicited STATUS, FETCH, and EXPUNGE responses for
// mailbox events.
//
// See Options.UnilateralDataHandler.
type UnilateralDataHandler struct {
Expunge func(seqNum uint32)
Mailbox func(data *UnilateralDataMailbox)
Fetch func(msg *FetchMessageData)

// requires ENABLE METADATA or ENABLE SERVER-METADATA
// Requires ENABLE METADATA or ENABLE SERVER-METADATA.
Metadata func(mailbox string, entries []string)

// Called when the server sends an unsolicited STATUS response.
//
// Commonly used with NOTIFY to receive mailbox status updates
// for non-selected mailboxes (RFC 5465).
Status func(data *imap.StatusData)

// Called when the server sends NOTIFICATIONOVERFLOW (RFC 5465).
//
// Indicates the server has disabled all NOTIFY notifications.
NotificationOverflow func()
}

// command is an interface for IMAP commands.
Expand Down
154 changes: 154 additions & 0 deletions imapclient/connection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package imapclient_test

import (
"io"
"net"
"testing"
"time"

"github.com/emersion/go-imap/v2/imapclient"
)

type pipeConn struct {
io.Reader
io.Writer
closer io.Closer
}

func (c pipeConn) Close() error {
return c.closer.Close()
}

func (c pipeConn) LocalAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}
}

func (c pipeConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}
}

func (c pipeConn) SetDeadline(t time.Time) error {
return nil
}

func (c pipeConn) SetReadDeadline(t time.Time) error {
return nil
}

func (c pipeConn) SetWriteDeadline(t time.Time) error {
return nil
}

var _ net.Conn = pipeConn{}

// TestCommand_Wait_ConnectionFailure tests that Wait() returns an error instead
// of hanging when the network connection drops unexpectedly.
func TestCommand_Wait_ConnectionFailure(t *testing.T) {
// Create a custom connection pair
clientR, serverW := io.Pipe()
serverR, clientW := io.Pipe()

clientConn := pipeConn{
Reader: clientR,
Writer: clientW,
closer: clientW,
}
serverConn := pipeConn{
Reader: serverR,
Writer: serverW,
closer: serverW,
}

client := imapclient.New(clientConn, nil)
defer client.Close()

// Hacky server which sends greeting then closes without responding to commands.
go func() {
serverW.Write([]byte("* OK IMAP server ready\r\n"))

buf := make([]byte, 1024)
serverR.Read(buf)

time.Sleep(50 * time.Millisecond)
serverConn.Close()
}()

if err := client.WaitGreeting(); err != nil {
t.Fatalf("WaitGreeting() = %v", err)
}

noopCmd := client.Noop()

// Wait should return an error, not hang
errCh := make(chan error, 1)
go func() {
errCh <- noopCmd.Wait()
}()

select {
case err := <-errCh:
if err == nil {
t.Error("Expected error after connection failure, got nil")
} else {
t.Logf("Wait() returned error as expected: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("Wait() hung after connection failure")
}
}

// TestMultipleCommands_ConnectionFailure tests that multiple pending commands
// are properly unblocked when the connection drops.
func TestMultipleCommands_ConnectionFailure(t *testing.T) {
// Create a custom connection pair
clientR, serverW := io.Pipe()
serverR, clientW := io.Pipe()

clientConn := pipeConn{
Reader: clientR,
Writer: clientW,
closer: clientW,
}
serverConn := pipeConn{
Reader: serverR,
Writer: serverW,
closer: serverW,
}

client := imapclient.New(clientConn, nil)
defer client.Close()

// Hacky server which send greeting then closes without responding.
go func() {
serverW.Write([]byte("* OK IMAP server ready\r\n"))

buf := make([]byte, 4096)
serverR.Read(buf)

time.Sleep(100 * time.Millisecond)
serverConn.Close()
}()

if err := client.WaitGreeting(); err != nil {
t.Fatalf("WaitGreeting() = %v", err)
}

cmd1 := client.Noop()
cmd2 := client.Noop()
cmd3 := client.Noop()

done := make(chan struct{})
go func() {
cmd1.Wait()
cmd2.Wait()
cmd3.Wait()
close(done)
}()

select {
case <-done:
t.Log("All commands completed after connection failure")
case <-time.After(5 * time.Second):
t.Fatal("Commands hung after connection failure")
}
}
99 changes: 99 additions & 0 deletions imapclient/notify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package imapclient

import (
"fmt"

"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapwire"
)

// Notify sends a NOTIFY command (RFC 5465).
//
// The NOTIFY command allows clients to request server-push notifications
// for mailbox events like new messages, expunges, flag changes, etc.
//
// When NOTIFY SET is active, the server may send unsolicited responses at any
// time (STATUS, FETCH, EXPUNGE, LIST responses). These unsolicited responses
// are delivered via the UnilateralDataHandler callbacks set in
// imapclient.Options.
//
// When the server sends NOTIFICATIONOVERFLOW, the NotificationOverflow callback
// in UnilateralDataHandler will be called (if set).
func (c *Client) Notify(options *imap.NotifyOptions) (*NotifyCommand, error) {
cmd := &NotifyCommand{}
enc := c.beginCommand("NOTIFY", cmd)
if err := encodeNotifyOptions(enc.Encoder, options); err != nil {
enc.end()
return nil, err
}
enc.end()

if err := cmd.Wait(); err != nil {
return nil, err
}

return cmd, nil
}

// encodeNotifyOptions encodes NOTIFY command options to the encoder.
func encodeNotifyOptions(enc *imapwire.Encoder, options *imap.NotifyOptions) error {
if options == nil || len(options.Items) == 0 {
// NOTIFY NONE: disable all notifications.
enc.SP().Atom("NONE")
return nil
}

enc.SP().Atom("SET")

if options.Status {
enc.SP().List(1, func(i int) {
enc.Atom("STATUS")
})
}

for _, item := range options.Items {
if item.MailboxSpec == "" && len(item.Mailboxes) == 0 {
return fmt.Errorf("invalid NOTIFY item: must specify either MailboxSpec or Mailboxes")
}

enc.SP().List(1, func(_ int) {
if item.MailboxSpec != "" {
enc.Atom(string(item.MailboxSpec))
} else {
// len(item.Mailboxes) > 0, as per the check above.
if item.Subtree {
enc.Atom("SUBTREE").SP()
}
enc.List(len(item.Mailboxes), func(j int) {
enc.Mailbox(item.Mailboxes[j])
})
}

if len(item.Events) > 0 {
enc.SP().List(len(item.Events), func(j int) {
enc.Atom(string(item.Events[j]))
})
}
})

}

return nil
}

// NotifyCommand is a NOTIFY command.
//
// When NOTIFY SET is active, the server may send unsolicited responses at any
// time. These responses are delivered via UnilateralDataHandler
// (see Options.UnilateralDataHandler).
//
// If the server sends NOTIFICATIONOVERFLOW, the NotificationOverflow callback
// in UnilateralDataHandler will be called (if set).
type NotifyCommand struct {
commandBase
}

// Wait blocks until the NOTIFY command has completed.
func (cmd *NotifyCommand) Wait() error {
return cmd.wait()
}
Loading