diff --git a/imapclient/client.go b/imapclient/client.go index 4933d2fa..6e105af6 100644 --- a/imapclient/client.go +++ b/imapclient/client.go @@ -905,6 +905,10 @@ func (c *Client) readResponseData(typ string) error { } case "NOMODSEQ": // ignore + case "NOTIFICATIONOVERFLOW": + if handler := c.options.unilateralDataHandler().NotificationOverflow; handler != nil { + handler() + } default: // [SP 1*] if c.dec.SP() { c.dec.DiscardUntilByte(']') @@ -1188,14 +1192,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. diff --git a/imapclient/client_test.go b/imapclient/client_test.go index 9e5c206f..84ea793c 100644 --- a/imapclient/client_test.go +++ b/imapclient/client_test.go @@ -125,6 +125,10 @@ func newMemClientServerPair(t *testing.T) (net.Conn, io.Closer) { } func newClientServerPair(t *testing.T, initialState imap.ConnState) (*imapclient.Client, io.Closer) { + return newClientServerPairWithOptions(t, initialState, nil) +} + +func newClientServerPairWithOptions(t *testing.T, initialState imap.ConnState, options *imapclient.Options) (*imapclient.Client, io.Closer) { var useDovecot bool switch os.Getenv("GOIMAP_TEST_DOVECOT") { case "0", "": @@ -151,11 +155,13 @@ func newClientServerPair(t *testing.T, initialState imap.ConnState) (*imapclient var debugWriter swapWriter debugWriter.Swap(io.Discard) - var options imapclient.Options + if options == nil { + options = &imapclient.Options{} + } if testing.Verbose() { options.DebugWriter = &debugWriter } - client := imapclient.New(conn, &options) + client := imapclient.New(conn, options) if initialState >= imap.ConnStateAuthenticated { // Dovecot connections are pre-authenticated diff --git a/imapclient/notify.go b/imapclient/notify.go new file mode 100644 index 00000000..d809be29 --- /dev/null +++ b/imapclient/notify.go @@ -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() +} diff --git a/imapclient/notify_encode_test.go b/imapclient/notify_encode_test.go new file mode 100644 index 00000000..fae049ac --- /dev/null +++ b/imapclient/notify_encode_test.go @@ -0,0 +1,306 @@ +package imapclient + +import ( + "bufio" + "bytes" + "testing" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/internal/imapwire" +) + +func encodeToString(options *imap.NotifyOptions) (string, error) { + buf := &bytes.Buffer{} + bw := bufio.NewWriter(buf) + enc := imapwire.NewEncoder(bw, imapwire.ConnSideClient) + + if err := encodeNotifyOptions(enc, options); err != nil { + return "", err + } + + enc.CRLF() + bw.Flush() + + return buf.String(), nil +} + +func TestEncodeNotifyOptions(t *testing.T) { + tests := []struct { + name string + options *imap.NotifyOptions + expected string + }{ + { + name: "None", + options: nil, + expected: " NONE\r\n", + }, + { + name: "EmptyItems", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{}, + }, + expected: " NONE\r\n", + }, + { + name: "Selected", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelected, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + }, + }, + }, + }, + expected: " SET (SELECTED (MessageNew MessageExpunge))\r\n", + }, + { + name: "SelectedDelayed", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelectedDelayed, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + }, + }, + }, + }, + expected: " SET (SELECTED-DELAYED (MessageNew MessageExpunge))\r\n", + }, + { + name: "Personal", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecPersonal, + Events: []imap.NotifyEvent{ + imap.NotifyEventMailboxName, + imap.NotifyEventSubscriptionChange, + }, + }, + }, + }, + expected: " SET (PERSONAL (MailboxName SubscriptionChange))\r\n", + }, + { + name: "Inboxes", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecInboxes, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + }, + }, + }, + }, + expected: " SET (INBOXES (MessageNew))\r\n", + }, + { + name: "Subscribed", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSubscribed, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMailboxName, + }, + }, + }, + }, + expected: " SET (SUBSCRIBED (MessageNew MailboxName))\r\n", + }, + { + name: "Subtree", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + Subtree: true, + Mailboxes: []string{"INBOX", "Lists"}, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + }, + }, + }, + }, + expected: ` SET (SUBTREE (INBOX "Lists") (MessageNew))` + "\r\n", + }, + { + name: "MailboxList", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + Mailboxes: []string{"INBOX", "Sent"}, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + imap.NotifyEventFlagChange, + }, + }, + }, + }, + expected: ` SET ((INBOX "Sent") (MessageNew MessageExpunge FlagChange))` + "\r\n", + }, + { + name: "StatusIndicator", + options: &imap.NotifyOptions{ + Status: true, + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelected, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + }, + }, + }, + }, + expected: " SET (STATUS) (SELECTED (MessageNew MessageExpunge))\r\n", + }, + { + name: "MultipleItems", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelected, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + }, + }, + { + MailboxSpec: imap.NotifyMailboxSpecPersonal, + Events: []imap.NotifyEvent{ + imap.NotifyEventMailboxName, + imap.NotifyEventSubscriptionChange, + }, + }, + { + MailboxSpec: imap.NotifyMailboxSpecInboxes, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + }, + }, + }, + }, + expected: " SET (SELECTED (MessageNew MessageExpunge)) (PERSONAL (MailboxName SubscriptionChange)) (INBOXES (MessageNew))\r\n", + }, + { + name: "AllEvents", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelected, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + imap.NotifyEventFlagChange, + imap.NotifyEventAnnotationChange, + imap.NotifyEventMailboxName, + imap.NotifyEventSubscriptionChange, + imap.NotifyEventMailboxMetadataChange, + imap.NotifyEventServerMetadataChange, + }, + }, + }, + }, + expected: " SET (SELECTED (MessageNew MessageExpunge FlagChange AnnotationChange MailboxName SubscriptionChange MailboxMetadataChange ServerMetadataChange))\r\n", + }, + { + name: "NoEvents", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelected, + Events: []imap.NotifyEvent{}, + }, + }, + }, + expected: " SET (SELECTED)\r\n", + }, + { + name: "ComplexMixed", + options: &imap.NotifyOptions{ + Status: true, + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelected, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + }, + }, + { + Subtree: true, + Mailboxes: []string{"INBOX"}, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + }, + }, + { + Mailboxes: []string{"Drafts", "Sent"}, + Events: []imap.NotifyEvent{ + imap.NotifyEventFlagChange, + }, + }, + }, + }, + expected: ` SET (STATUS) (SELECTED (MessageNew MessageExpunge)) (SUBTREE (INBOX) (MessageNew)) (("Drafts" "Sent") (FlagChange))` + "\r\n", + }, + { + name: "MailboxWithSpecialChars", + options: &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + Mailboxes: []string{"INBOX", "Foo Bar", "Test&Mailbox"}, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + }, + }, + }, + }, + expected: ` SET ((INBOX "Foo Bar" "Test&-Mailbox") (MessageNew))` + "\r\n", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := encodeToString(tc.options) + if err != nil { + t.Fatalf("encodeToString() error = %v", err) + } + if result != tc.expected { + t.Errorf("Expected %q, got %q", tc.expected, result) + } + }) + } +} + +func TestEncodeNotifyOptions_InvalidItem(t *testing.T) { + // Items with neither MailboxSpec nor Mailboxes should return an error + options := &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + // Invalid: no mailbox spec or mailboxes + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + }, + }, + }, + } + _, err := encodeToString(options) + if err == nil { + t.Fatal("Expected error for invalid NOTIFY item, got nil") + } + + expectedMsg := "invalid NOTIFY item: must specify either MailboxSpec or Mailboxes" + if err.Error() != expectedMsg { + t.Errorf("Expected error %q, got %q", expectedMsg, err.Error()) + } +} diff --git a/imapclient/notify_test.go b/imapclient/notify_test.go new file mode 100644 index 00000000..42585e2e --- /dev/null +++ b/imapclient/notify_test.go @@ -0,0 +1,337 @@ +package imapclient_test + +import ( + "testing" + "time" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapclient" +) + +func TestClient_Notify(t *testing.T) { + existsCh := make(chan uint32, 1) + + options := &imapclient.Options{ + UnilateralDataHandler: &imapclient.UnilateralDataHandler{ + Expunge: func(seqNum uint32) { + // Not testing expunge in this test + }, + Mailbox: func(data *imapclient.UnilateralDataMailbox) { + if data.NumMessages != nil { + select { + case existsCh <- *data.NumMessages: + default: + } + } + }, + }, + } + + client, server := newClientServerPairWithOptions(t, imap.ConnStateSelected, options) + defer client.Close() + defer server.Close() + + if !client.Caps().Has(imap.CapNotify) { + t.Skip("NOTIFY not supported") + } + + selectData, err := client.Select("INBOX", nil).Wait() + if err != nil { + t.Fatalf("Select() = %v", err) + } + initialExists := selectData.NumMessages + + notifyOptions := &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelected, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + }, + }, + }, + } + cmd, err := client.Notify(notifyOptions) + if err != nil { + t.Fatalf("Notify() = %v", err) + } + if cmd == nil { + t.Fatal("Expected non-nil NotifyCommand") + } + + // Append a new message to INBOX (we should get a NOTIFY event for it). + testMessage := `From: sender@example.com +To: recipient@example.com +Subject: Test NOTIFY + +This is a test message for NOTIFY. +` + appendCmd := client.Append("INBOX", int64(len(testMessage)), nil) + appendCmd.Write([]byte(testMessage)) + appendCmd.Close() + if _, err := appendCmd.Wait(); err != nil { + t.Fatalf("Append() = %v", err) + } + + // Wait for the EXISTS notification (with timeout) + select { + case count := <-existsCh: + if count <= initialExists { + t.Errorf("Expected EXISTS count > %d, got %d", initialExists, count) + } + t.Logf("Received EXISTS notification: %d messages (was %d)", count, initialExists) + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for EXISTS notification") + } +} + +func TestClient_NotifyNone(t *testing.T) { + client, server := newClientServerPair(t, imap.ConnStateAuthenticated) + defer client.Close() + defer server.Close() + + if !client.Caps().Has(imap.CapNotify) { + t.Skip("NOTIFY not supported") + } + + _, err := client.Notify(nil) + if err != nil { + t.Fatalf("NotifyNone() = %v", err) + } +} + +func TestClient_NotifyMultiple(t *testing.T) { + client, server := newClientServerPair(t, imap.ConnStateAuthenticated) + defer client.Close() + defer server.Close() + + if !client.Caps().Has(imap.CapNotify) { + t.Skip("NOTIFY not supported") + } + + // Test NOTIFY with multiple items + // Note: Dovecot doesn't support STATUS with message events + options := &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelected, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + }, + }, + { + MailboxSpec: imap.NotifyMailboxSpecPersonal, + Events: []imap.NotifyEvent{ + imap.NotifyEventMailboxName, + imap.NotifyEventSubscriptionChange, + }, + }, + }, + } + + cmd, err := client.Notify(options) + if err != nil { + t.Fatalf("Notify() = %v", err) + } + + if cmd == nil { + t.Fatal("Expected non-nil NotifyCommand") + } +} + +// TestClient_NotifyPersonalMailboxes tests NOTIFY for personal mailboxes +func TestClient_NotifyPersonalMailboxes(t *testing.T) { + client, server := newClientServerPair(t, imap.ConnStateAuthenticated) + defer client.Close() + defer server.Close() + + if !client.Caps().Has(imap.CapNotify) { + t.Skip("NOTIFY not supported") + } + + // Note: Dovecot doesn't support message events with PERSONAL spec + // Only mailbox events (MailboxName, SubscriptionChange) seem to work. + options := &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecPersonal, + Events: []imap.NotifyEvent{ + imap.NotifyEventMailboxName, + imap.NotifyEventSubscriptionChange, + }, + }, + }, + } + + cmd, err := client.Notify(options) + if err != nil { + t.Fatalf("Notify() = %v", err) + } + + if cmd == nil { + t.Fatal("Expected non-nil NotifyCommand") + } +} + +// TestClient_NotifySubtree tests NOTIFY for mailbox subtrees +func TestClient_NotifySubtree(t *testing.T) { + client, server := newClientServerPair(t, imap.ConnStateAuthenticated) + defer client.Close() + defer server.Close() + + if !client.Caps().Has(imap.CapNotify) { + t.Skip("NOTIFY not supported") + } + + // Request notifications for INBOX subtree + options := &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + Mailboxes: []string{"INBOX"}, + Subtree: true, + Events: []imap.NotifyEvent{ + imap.NotifyEventMailboxName, + imap.NotifyEventSubscriptionChange, + }, + }, + }, + } + + cmd, err := client.Notify(options) + if err != nil { + t.Fatalf("Notify() = %v", err) + } + + if cmd == nil { + t.Fatal("Expected non-nil NotifyCommand") + } +} + +// TestClient_NotifyMailboxes tests NOTIFY for specific mailboxes with message events +func TestClient_NotifyMailboxes(t *testing.T) { + client, server := newClientServerPair(t, imap.ConnStateAuthenticated) + defer client.Close() + defer server.Close() + + if !client.Caps().Has(imap.CapNotify) { + t.Skip("NOTIFY not supported") + } + + // Request notifications for specific mailboxes with SUBTREE + // Note: Dovecot requires SUBTREE for explicit mailbox specifications + options := &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + Mailboxes: []string{"INBOX"}, + Subtree: true, + Events: []imap.NotifyEvent{ + imap.NotifyEventMailboxName, + imap.NotifyEventSubscriptionChange, + }, + }, + }, + } + + cmd, err := client.Notify(options) + if err != nil { + t.Fatalf("Notify() = %v", err) + } + + if cmd == nil { + t.Fatal("Expected non-nil NotifyCommand") + } +} + +// TestClient_NotifySelectedDelayed tests NOTIFY with SELECTED-DELAYED for safe MSN usage +func TestClient_NotifySelectedDelayed(t *testing.T) { + client, server := newClientServerPair(t, imap.ConnStateSelected) + defer client.Close() + defer server.Close() + + if !client.Caps().Has(imap.CapNotify) { + t.Skip("NOTIFY not supported") + } + + // Request notifications with SELECTED-DELAYED to defer expunge notifications + options := &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelectedDelayed, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + }, + }, + }, + } + + cmd, err := client.Notify(options) + if err != nil { + t.Fatalf("Notify() = %v", err) + } + + if cmd == nil { + t.Fatal("Expected non-nil NotifyCommand") + } +} + +// TestClient_NotifySequence tests a sequence of NOTIFY commands +func TestClient_NotifySequence(t *testing.T) { + client, server := newClientServerPair(t, imap.ConnStateAuthenticated) + defer client.Close() + defer server.Close() + + if !client.Caps().Has(imap.CapNotify) { + t.Skip("NOTIFY not supported") + } + + // First NOTIFY command + options1 := &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecSelected, + Events: []imap.NotifyEvent{ + imap.NotifyEventMessageNew, + imap.NotifyEventMessageExpunge, + }, + }, + }, + } + + cmd1, err := client.Notify(options1) + if err != nil { + t.Fatalf("First Notify() = %v", err) + } + if cmd1 == nil { + t.Fatal("Expected non-nil NotifyCommand from first call") + } + + // Replace with different NOTIFY settings + options2 := &imap.NotifyOptions{ + Items: []imap.NotifyItem{ + { + MailboxSpec: imap.NotifyMailboxSpecPersonal, + Events: []imap.NotifyEvent{ + imap.NotifyEventMailboxName, + imap.NotifyEventSubscriptionChange, + }, + }, + }, + } + + cmd2, err := client.Notify(options2) + if err != nil { + t.Fatalf("Second Notify() = %v", err) + } + if cmd2 == nil { + t.Fatal("Expected non-nil NotifyCommand from second call") + } + + // Disable all notifications + _, err = client.Notify(nil) + if err != nil { + t.Fatalf("NotifyNone() = %v", err) + } +} diff --git a/imapclient/status.go b/imapclient/status.go index 973345bc..b9bf6374 100644 --- a/imapclient/status.go +++ b/imapclient/status.go @@ -68,6 +68,13 @@ func (c *Client) handleStatus() error { return false } }) + if cmd == nil { + // Unsolicited STATUS response (e.g., from NOTIFY) + if handler := c.options.unilateralDataHandler().Status; handler != nil { + handler(data) + } + return nil + } switch cmd := cmd.(type) { case *StatusCommand: cmd.data = *data diff --git a/notify.go b/notify.go new file mode 100644 index 00000000..776ca63c --- /dev/null +++ b/notify.go @@ -0,0 +1,57 @@ +package imap + +// NotifyEvent represents an event type for the NOTIFY command (RFC 5465). +type NotifyEvent string + +const ( + // Message events + NotifyEventFlagChange NotifyEvent = "FlagChange" + NotifyEventAnnotationChange NotifyEvent = "AnnotationChange" + NotifyEventMessageNew NotifyEvent = "MessageNew" + NotifyEventMessageExpunge NotifyEvent = "MessageExpunge" + + // Mailbox events + NotifyEventMailboxName NotifyEvent = "MailboxName" + NotifyEventSubscriptionChange NotifyEvent = "SubscriptionChange" + NotifyEventMailboxMetadataChange NotifyEvent = "MailboxMetadataChange" + NotifyEventServerMetadataChange NotifyEvent = "ServerMetadataChange" +) + +// NotifyMailboxSpec represents a mailbox specifier RFC 5465 section 6 for the NOTIFY command. +type NotifyMailboxSpec string + +const ( + NotifyMailboxSpecSelected NotifyMailboxSpec = "SELECTED" + NotifyMailboxSpecSelectedDelayed NotifyMailboxSpec = "SELECTED-DELAYED" + NotifyMailboxSpecPersonal NotifyMailboxSpec = "PERSONAL" + NotifyMailboxSpecInboxes NotifyMailboxSpec = "INBOXES" + NotifyMailboxSpecSubscribed NotifyMailboxSpec = "SUBSCRIBED" +) + +// NotifyOptions contains options for the NOTIFY command. +type NotifyOptions struct { + // Status indicates that a STATUS response should be sent for new mailboxes. + // Only valid with Personal, Inboxes, or Subscribed mailbox specs. + Status bool + + // Items represents the mailbox and events to monitor. + Items []NotifyItem +} + +// NotifyItem represents a mailbox or mailbox set and its events. +type NotifyItem struct { + // MailboxSpec is a special mailbox specifier (Selected, Personal, etc.) + // If empty, Mailboxes must be non-empty. + MailboxSpec NotifyMailboxSpec + + // Mailboxes is a list of specific mailboxes to monitor. + // Can include wildcards (*). + Mailboxes []string + + // Subtree indicates that all mailboxes under the specified mailboxes + // should be monitored (recursive). + Subtree bool + + // Events is the list of events to monitor for these mailboxes. + Events []NotifyEvent +}