imapserver: implement NOTIFY extension from RFC 5465

NOTIFY is like IDLE, but where IDLE watches just the selected mailbox, NOTIFY
can watch all mailboxes. With NOTIFY, a client can also ask a server to
immediately return configurable fetch attributes for new messages, e.g. a
message preview, certain header fields, or simply the entire message.

Mild testing with evolution and fairemail.
This commit is contained in:
Mechiel Lukkien
2025-04-07 23:21:03 +02:00
parent 5a7d5fce98
commit 8bab38eac4
30 changed files with 1926 additions and 161 deletions

View File

@ -185,13 +185,18 @@ func (c *Conn) xflush() {
}
}
func (c *Conn) xtrace(level slog.Level) func() {
c.xflush()
func (c *Conn) xtraceread(level slog.Level) func() {
c.tr.SetTrace(level)
return func() {
c.tr.SetTrace(mlog.LevelTrace)
}
}
func (c *Conn) xtracewrite(level slog.Level) func() {
c.xflush()
c.xtw.SetTrace(level)
return func() {
c.xflush()
c.tr.SetTrace(mlog.LevelTrace)
c.xtw.SetTrace(mlog.LevelTrace)
}
}
@ -357,9 +362,10 @@ func (c *Conn) WriteSyncLiteral(s string) (untagged []Untagged, rerr error) {
_, err = c.Readline()
c.xcheckf(err, "read continuation line")
defer c.xtracewrite(mlog.LevelTracedata)()
_, err = c.xbw.Write([]byte(s))
c.xcheckf(err, "write literal data")
c.xflush()
c.xtracewrite(mlog.LevelTrace)
return nil, nil
}
untagged, result, err := c.Response()

View File

@ -59,9 +59,9 @@ func (c *Conn) Login(username, password string) (untagged []Untagged, result Res
c.LastTag = c.nextTag()
fmt.Fprintf(c.xbw, "%s login %s ", c.LastTag, astring(username))
defer c.xtrace(mlog.LevelTraceauth)()
defer c.xtracewrite(mlog.LevelTraceauth)()
fmt.Fprintf(c.xbw, "%s\r\n", astring(password))
c.xtrace(mlog.LevelTrace) // Restore.
c.xtracewrite(mlog.LevelTrace) // Restore.
return c.Response()
}
@ -76,11 +76,11 @@ func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged
if result.Status != "" {
c.xerrorf("got result status %q, expected continuation", result.Status)
}
defer c.xtrace(mlog.LevelTraceauth)()
defer c.xtracewrite(mlog.LevelTraceauth)()
xw := base64.NewEncoder(base64.StdEncoding, c.xbw)
fmt.Fprintf(xw, "\u0000%s\u0000%s", username, password)
xw.Close()
c.xtrace(mlog.LevelTrace) // Restore.
c.xtracewrite(mlog.LevelTrace) // Restore.
fmt.Fprintf(c.xbw, "\r\n")
c.xflush()
return c.Response()
@ -317,10 +317,10 @@ func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged
// todo: for larger messages, use a synchronizing literal.
fmt.Fprintf(c.xbw, " (%s)%s {%d+}\r\n", strings.Join(m.Flags, " "), date, m.Size)
defer c.xtrace(mlog.LevelTracedata)()
defer c.xtracewrite(mlog.LevelTracedata)()
_, err := io.Copy(c.xbw, m.Data)
c.xcheckf(err, "write message data")
c.xtrace(mlog.LevelTrace) // Restore
c.xtracewrite(mlog.LevelTrace) // Restore
}
fmt.Fprintf(c.xbw, "\r\n")
@ -328,7 +328,8 @@ func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged
return c.Response()
}
// note: No idle command. Idle is better implemented by writing the request and reading and handling the responses as they come in.
// note: No Idle or Notify command. Idle/Notify is better implemented by
// writing the request and reading and handling the responses as they come in.
// CloseMailbox closes the currently selected/active mailbox, permanently removing
// any messages marked with \Deleted.
@ -444,10 +445,10 @@ func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (unta
err := c.Commandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size)
c.xcheckf(err, "writing replace command")
defer c.xtrace(mlog.LevelTracedata)()
defer c.xtracewrite(mlog.LevelTracedata)()
_, err = io.Copy(c.xbw, msg.Data)
c.xcheckf(err, "write message data")
c.xtrace(mlog.LevelTrace)
c.xtracewrite(mlog.LevelTrace)
fmt.Fprintf(c.xbw, "\r\n")
c.xflush()

View File

@ -6,6 +6,8 @@ import (
"strconv"
"strings"
"time"
"github.com/mjl-/mox/mlog"
)
func (c *Conn) recorded() string {
@ -131,7 +133,9 @@ var knownCodes = stringMap(
// With parameters.
"BADCHARSET", "CAPABILITY", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "APPENDUID", "COPYUID",
"HIGHESTMODSEQ", "MODIFIED",
"INPROGRESS", // ../rfc/9585:104
"INPROGRESS", // ../rfc/9585:104
"BADEVENT", "NOTIFICATIONOVERFLOW", // ../rfc/5465:1023
"SERVERBUG",
)
func stringMap(l ...string) map[string]struct{} {
@ -247,6 +251,20 @@ func (c *Conn) xrespCode() (string, CodeArg) {
c.xtake(")")
}
codeArg = CodeInProgress{tag, current, goal}
case "BADEVENT":
// ../rfc/5465:1033
c.xspace()
c.xtake("(")
var l []string
for {
s := c.xatom()
l = append(l, s)
if !c.space() {
break
}
}
c.xtake(")")
codeArg = CodeBadEvent(l)
}
return W, codeArg
}
@ -896,8 +914,10 @@ func (c *Conn) xliteral() []byte {
c.xflush()
}
buf := make([]byte, int(size))
defer c.xtraceread(mlog.LevelTracedata)()
_, err := io.ReadFull(c.br, buf)
c.xcheckf(err, "reading data for literal")
c.xtraceread(mlog.LevelTrace)
return buf
}

View File

@ -42,6 +42,7 @@ const (
CapReplace Capability = "REPLACE" // ../rfc/8508:155
CapPreview Capability = "PREVIEW" // ../rfc/8970:114
CapMultiSearch Capability = "MULTISEARCH" // ../rfc/7377:187
CapNotify Capability = "NOTIFY" // ../rfc/5465:195
)
// Status is the tagged final result of a command.
@ -186,6 +187,14 @@ func (c CodeInProgress) CodeString() string {
return fmt.Sprintf("INPROGRESS (%q %s %s)", c.Tag, current, goal)
}
// "BADEVENT" response code, with the events that are supported, for the NOTIFY
// extension.
type CodeBadEvent []string
func (c CodeBadEvent) CodeString() string {
return fmt.Sprintf("BADEVENT (%s)", strings.Join([]string(c), " "))
}
// RespText represents a response line minus the leading tag.
type RespText struct {
Code string // The first word between [] after the status.