From 63de5e81e477ba08734b7cf02b8b278e45f0c19c Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Sat, 10 Jul 2021 11:42:55 +0100 Subject: [PATCH] Very early NOTIFY support, hopefully fix -password on Windows --- cmd/yggmail/main.go | 42 +++++++---------- internal/imapserver/backend.go | 21 +++++++++ internal/imapserver/imap.go | 24 +++++++++- internal/imapserver/notify.go | 65 +++++++++++++++++++++++++++ internal/imapserver/user.go | 2 + internal/smtpserver/backend.go | 2 + internal/smtpserver/session_remote.go | 11 ++++- internal/smtpserver/smtp.go | 5 ++- 8 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 internal/imapserver/notify.go diff --git a/cmd/yggmail/main.go b/cmd/yggmail/main.go index a8f0c86..29ba257 100644 --- a/cmd/yggmail/main.go +++ b/cmd/yggmail/main.go @@ -10,8 +10,8 @@ import ( "os" "strings" "sync" + "syscall" - "github.com/emersion/go-imap/server" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "golang.org/x/term" @@ -86,13 +86,13 @@ func main() { switch { case password != nil && *password: log.Println("Please enter your new password:") - password1, err := term.ReadPassword(0) + password1, err := term.ReadPassword(syscall.Stdin) if err != nil { panic(err) } fmt.Println() log.Println("Please enter your new password again:") - password2, err := term.ReadPassword(0) + password2, err := term.ReadPassword(syscall.Stdin) if err != nil { panic(err) } @@ -127,31 +127,19 @@ func main() { } queues := smtpsender.NewQueues(cfg, log, transport, storage) + var notify *imapserver.IMAPNotify - go func() { - defer wg.Done() + imapBackend := &imapserver.Backend{ + Log: log, + Config: cfg, + Storage: storage, + } - imapBackend := &imapserver.Backend{ - Log: log, - Config: cfg, - Storage: storage, - } - - imapServer := server.New(imapBackend) - imapServer.Addr = *imapaddr - imapServer.AllowInsecureAuth = true - imapServer.EnableAuth(sasl.Login, func(conn server.Conn) sasl.Server { - return sasl.NewLoginServer(func(username, password string) error { - _, err := imapBackend.Login(nil, username, password) - return err - }) - }) - - log.Println("Listening for IMAP on:", imapServer.Addr) - if err := imapServer.ListenAndServe(); err != nil { - log.Fatal(err) - } - }() + _, notify, err = imapserver.NewIMAPServer(imapBackend, *imapaddr, true) + if err != nil { + log.Fatal(err) + } + log.Println("Listening for IMAP on:", *imapaddr) go func() { defer wg.Done() @@ -162,6 +150,7 @@ func main() { Config: cfg, Storage: storage, Queues: queues, + Notify: notify, } localServer := smtp.NewServer(localBackend) @@ -192,6 +181,7 @@ func main() { Config: cfg, Storage: storage, Queues: queues, + Notify: notify, } overlayServer := smtp.NewServer(overlayBackend) diff --git a/internal/imapserver/backend.go b/internal/imapserver/backend.go index 9bc6f7a..5529f61 100644 --- a/internal/imapserver/backend.go +++ b/internal/imapserver/backend.go @@ -16,6 +16,7 @@ type Backend struct { Config *config.Config Log *log.Logger Storage storage.Storage + Server *IMAPServer } func (b *Backend) Login(conn *imap.ConnInfo, username, password string) (backend.User, error) { @@ -38,6 +39,26 @@ func (b *Backend) Login(conn *imap.ConnInfo, username, password string) (backend user := &User{ backend: b, username: username, + conn: conn, } return user, nil } + +/* +func (b *Backend) NotifyNew(id int) error { + b.Server.server.ForEachConn(func(conn server.Conn) { + notify := false + for _, cap := range conn.Capabilities() { + if cap == "NOTIFY" { + notify = true + } + } + if !notify { + return + } + conn.WaitReady() + conn.WriteResp() + }) + return nil +} +*/ diff --git a/internal/imapserver/imap.go b/internal/imapserver/imap.go index 254a385..f20d4bc 100644 --- a/internal/imapserver/imap.go +++ b/internal/imapserver/imap.go @@ -1,8 +1,12 @@ package imapserver import ( + "log" + "os" + idle "github.com/emersion/go-imap-idle" "github.com/emersion/go-imap/server" + "github.com/emersion/go-sasl" ) type IMAPServer struct { @@ -10,11 +14,27 @@ type IMAPServer struct { backend *Backend } -func NewIMAPServer(backend *Backend) (*IMAPServer, error) { +func NewIMAPServer(backend *Backend, addr string, insecure bool) (*IMAPServer, *IMAPNotify, error) { s := &IMAPServer{ server: server.New(backend), backend: backend, } + notify := NewIMAPNotify(s.server, backend.Log) + s.server.Addr = addr + s.server.AllowInsecureAuth = insecure + s.server.Debug = os.Stdout s.server.Enable(idle.NewExtension()) - return s, nil + s.server.Enable(notify) + s.server.EnableAuth(sasl.Login, func(conn server.Conn) sasl.Server { + return sasl.NewLoginServer(func(username, password string) error { + _, err := s.backend.Login(nil, username, password) + return err + }) + }) + go func() { + if err := s.server.ListenAndServe(); err != nil { + log.Fatal(err) + } + }() + return s, notify, nil } diff --git a/internal/imapserver/notify.go b/internal/imapserver/notify.go new file mode 100644 index 0000000..9651492 --- /dev/null +++ b/internal/imapserver/notify.go @@ -0,0 +1,65 @@ +package imapserver + +import ( + "fmt" + "log" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/server" +) + +type IMAPNotifyHandler struct { + imap.Command +} + +func (h *IMAPNotifyHandler) Handle(conn server.Conn) error { + // TODO: Support setting NOTIFY subscriptions or not + return nil +} + +type IMAPNotify struct { + server *server.Server + log *log.Logger +} + +func (ext *IMAPNotify) Capabilities(c server.Conn) []string { + if c.Context().State&imap.AuthenticatedState != 0 { + return []string{"NOTIFY"} + } + return nil +} + +func (ext *IMAPNotify) Command(name string) server.HandlerFactory { + if name != "NOTIFY" { + return nil + } + return func() server.Handler { + return &IMAPNotifyHandler{} + } +} + +func (ext *IMAPNotify) NotifyNew(id, count int) error { + ext.server.ForEachConn(func(c server.Conn) { + var resptype imap.StatusRespType + if mailbox := c.Context().Mailbox; mailbox != nil && mailbox.Name() == "INBOX" { + resptype = imap.StatusRespType( + fmt.Sprintf("EXISTS %d", id), + ) + } else { + resptype = imap.StatusRespType( + fmt.Sprintf("STATUS INBOX (UIDNEXT %d MESSAGES %d)", id+1, count), + ) + } + _ = c.WriteResp(&imap.StatusResp{ + Type: resptype, + }) + }) + return nil +} + +func NewIMAPNotify(s *server.Server, log *log.Logger) *IMAPNotify { + return &IMAPNotify{ + server: s, + log: log, + } +} diff --git a/internal/imapserver/user.go b/internal/imapserver/user.go index 32bdaae..61b7e6b 100644 --- a/internal/imapserver/user.go +++ b/internal/imapserver/user.go @@ -5,12 +5,14 @@ import ( "errors" "fmt" + "github.com/emersion/go-imap" "github.com/emersion/go-imap/backend" ) type User struct { backend *Backend username string + conn *imap.ConnInfo } func (u *User) Username() string { diff --git a/internal/smtpserver/backend.go b/internal/smtpserver/backend.go index 5279074..a9369b6 100644 --- a/internal/smtpserver/backend.go +++ b/internal/smtpserver/backend.go @@ -7,6 +7,7 @@ import ( "github.com/emersion/go-smtp" "github.com/neilalexander/yggmail/internal/config" + "github.com/neilalexander/yggmail/internal/imapserver" "github.com/neilalexander/yggmail/internal/smtpsender" "github.com/neilalexander/yggmail/internal/storage" "github.com/neilalexander/yggmail/internal/utils" @@ -25,6 +26,7 @@ type Backend struct { Config *config.Config Queues *smtpsender.Queues Storage storage.Storage + Notify *imapserver.IMAPNotify } func (b *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { diff --git a/internal/smtpserver/session_remote.go b/internal/smtpserver/session_remote.go index 559d100..59a6012 100644 --- a/internal/smtpserver/session_remote.go +++ b/internal/smtpserver/session_remote.go @@ -65,10 +65,17 @@ func (s *SessionRemote) Data(r io.Reader) error { return fmt.Errorf("m.WriteTo: %w", err) } - if _, err := s.backend.Storage.MailCreate("INBOX", b.Bytes()); err != nil { + if id, err := s.backend.Storage.MailCreate("INBOX", b.Bytes()); err != nil { return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err) + } else { + s.backend.Log.Printf("Stored new mail from %s", s.from) + + if count, err := s.backend.Storage.MailCount("INBOX"); err == nil { + if err := s.backend.Notify.NotifyNew(id, count); err != nil { + s.backend.Log.Println("Failed to notify:", s.from) + } + } } - s.backend.Log.Printf("Stored new mail from %s", s.from) return nil } diff --git a/internal/smtpserver/smtp.go b/internal/smtpserver/smtp.go index 8762536..391ffde 100644 --- a/internal/smtpserver/smtp.go +++ b/internal/smtpserver/smtp.go @@ -2,17 +2,20 @@ package smtpserver import ( "github.com/emersion/go-smtp" + "github.com/neilalexander/yggmail/internal/imapserver" ) type SMTPServer struct { server *smtp.Server backend smtp.Backend + notify *imapserver.IMAPNotify } -func NewSMTPServer(backend smtp.Backend) *SMTPServer { +func NewSMTPServer(backend smtp.Backend, notify *imapserver.IMAPNotify) *SMTPServer { s := &SMTPServer{ server: smtp.NewServer(backend), backend: backend, + notify: notify, } return s }