From 49e2eba52bb026e163d128f1bc17ceea0b4966f1 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Sat, 25 Jan 2025 22:18:26 +0100 Subject: [PATCH] add cli command "mox admin imapserve $preauthaddress" for admins to open an imap connection preauthenticated for an account (by address), also when it is disabled for logins. useful for migrations. the admin typically doesn't know the password of the account, so couldn't get an imap session (for synchronizing) before. tested with "mox localserve" and running: mutt -e 'set tunnel="MOXCONF=/home/mjl/.config/mox-localserve/mox.conf ./mox admin imapserve mox@localhost"' may also work with interimap, but untested. i initially assumed imap would be done fully on file descriptor 0, but mutt expects imap output on fd 1. that's the default now. flag -fd0 is for others that expect it on fd0. for issue #175, suggested by DanielG --- ctl.go | 19 ++++++++++-- ctl_test.go | 19 +++++++++++- doc.go | 13 ++++++++ imapserver/authenticate_test.go | 2 +- imapserver/fuzz_test.go | 2 +- imapserver/server.go | 38 ++++++++++++++++++++--- imapserver/server_test.go | 2 +- import.go | 2 +- localserve.go | 2 +- main.go | 54 +++++++++++++++++++++++++++++++++ serve_unix.go | 2 +- 11 files changed, 141 insertions(+), 14 deletions(-) diff --git a/ctl.go b/ctl.go index 9d0f747..0489194 100644 --- a/ctl.go +++ b/ctl.go @@ -25,6 +25,7 @@ import ( "github.com/mjl-/mox/admin" "github.com/mjl-/mox/config" "github.com/mjl-/mox/dns" + "github.com/mjl-/mox/imapserver" "github.com/mjl-/mox/message" "github.com/mjl-/mox/metrics" "github.com/mjl-/mox/mlog" @@ -277,7 +278,7 @@ func (s *ctlreader) xcheck(err error, msg string) { } // servectl handles requests on the unix domain socket "ctl", e.g. for graceful shutdown, local mail delivery. -func servectl(ctx context.Context, log mlog.Log, conn net.Conn, shutdown func()) { +func servectl(ctx context.Context, cid int64, log mlog.Log, conn net.Conn, shutdown func()) { log.Debug("ctl connection") var stop = struct{}{} // Sentinel value for panic and recover. @@ -296,7 +297,7 @@ func servectl(ctx context.Context, log mlog.Log, conn net.Conn, shutdown func()) ctl.xwrite("ctlv0") for { - servectlcmd(ctx, ctl, shutdown) + servectlcmd(ctx, ctl, cid, shutdown) } } @@ -307,7 +308,7 @@ func xparseJSON(ctl *ctl, s string, v any) { ctl.xcheck(err, "parsing from ctl as json") } -func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) { +func servectlcmd(ctx context.Context, ctl *ctl, cid int64, shutdown func()) { log := ctl.log cmd := ctl.xread() ctl.cmd = cmd @@ -1824,6 +1825,18 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) { case "backup": backupctl(ctx, ctl) + case "imapserve": + /* protocol: + > "imapserve" + > address + < "ok or error" + imap protocol + */ + address := ctl.xread() + ctl.xwriteok() + imapserver.ServeConnPreauth("(imapserve)", cid, ctl.conn, address) + ctl.log.Debug("imap connection finished") + default: log.Info("unrecognized command", slog.String("cmd", cmd)) ctl.xwrite("unrecognized command") diff --git a/ctl_test.go b/ctl_test.go index cd73f92..d5635b4 100644 --- a/ctl_test.go +++ b/ctl_test.go @@ -19,6 +19,7 @@ import ( "github.com/mjl-/mox/config" "github.com/mjl-/mox/dmarcdb" "github.com/mjl-/mox/dns" + "github.com/mjl-/mox/imapclient" "github.com/mjl-/mox/mlog" "github.com/mjl-/mox/mox-" "github.com/mjl-/mox/mtastsdb" @@ -58,6 +59,8 @@ func TestCtl(t *testing.T) { tcheck(t, err, "store init") defer store.Close() + var cid int64 + testctl := func(fn func(clientctl *ctl)) { t.Helper() @@ -66,7 +69,8 @@ func TestCtl(t *testing.T) { serverctl := ctl{conn: sconn, log: pkglog} done := make(chan struct{}) go func() { - servectlcmd(ctxbg, &serverctl, func() {}) + cid++ + servectlcmd(ctxbg, &serverctl, cid, func() {}) close(done) }() fn(&clientctl) @@ -513,6 +517,19 @@ func TestCtl(t *testing.T) { flagArgs: []string{filepath.FromSlash("testdata/ctl/data/tmp/backup/data")}, } cmdVerifydata(&xcmd) + + // IMAP connection. + testctl(func(ctl *ctl) { + a, b := net.Pipe() + go func() { + client, err := imapclient.New(a, true) + tcheck(t, err, "new imapclient") + client.Select("inbox") + client.Logout() + defer a.Close() + }() + ctlcmdIMAPServe(ctl, "mjl@mox.example", b, b) + }) } func fakeCert(t *testing.T) []byte { diff --git a/doc.go b/doc.go index 274c273..214f402 100644 --- a/doc.go +++ b/doc.go @@ -89,6 +89,7 @@ any parameters. Followed by the help and usage information for each command. mox config printservice >mox.service mox config ensureacmehostprivatekeys mox config example [name] + mox admin imapserve preauth-address mox checkupdate mox cid cid mox clientconfig domain @@ -1204,6 +1205,18 @@ List available config examples, or print a specific example. usage: mox config example [name] +# mox admin imapserve + +Initiate a preauthenticated IMAP connection on file descriptor 0. + +For use with tools that can do IMAP over tunneled connections, e.g. with SSH +during migrations. TLS is not possible on the connection, and authentication +does not require TLS. + + usage: mox admin imapserve preauth-address + -fd0 + write IMAP to file descriptor 0 instead of stdout + # mox checkupdate Check if a newer version of mox is available. diff --git a/imapserver/authenticate_test.go b/imapserver/authenticate_test.go index 0cf914d..6011bf7 100644 --- a/imapserver/authenticate_test.go +++ b/imapserver/authenticate_test.go @@ -368,7 +368,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) { cid := connCounter go func() { defer serverConn.Close() - serve("test", cid, &serverConfig, serverConn, true, false, false) + serve("test", cid, &serverConfig, serverConn, true, false, false, "") close(done) }() diff --git a/imapserver/fuzz_test.go b/imapserver/fuzz_test.go index af9e152..3a3f573 100644 --- a/imapserver/fuzz_test.go +++ b/imapserver/fuzz_test.go @@ -133,7 +133,7 @@ func FuzzServer(f *testing.F) { err = serverConn.SetDeadline(time.Now().Add(time.Second)) flog(err, "set server deadline") - serve("test", cid, nil, serverConn, false, true, false) + serve("test", cid, nil, serverConn, false, true, false, "") cid++ } diff --git a/imapserver/server.go b/imapserver/server.go index 45521a9..29f5230 100644 --- a/imapserver/server.go +++ b/imapserver/server.go @@ -381,7 +381,7 @@ func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, } metricIMAPConnection.WithLabelValues(protocol).Inc() - go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS, false) + go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS, false, "") } } @@ -390,7 +390,11 @@ func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, // ServeTLSConn serves IMAP on a TLS connection. func ServeTLSConn(listenerName string, conn *tls.Conn, tlsConfig *tls.Config) { - serve(listenerName, mox.Cid(), tlsConfig, conn, true, false, true) + serve(listenerName, mox.Cid(), tlsConfig, conn, true, false, true, "") +} + +func ServeConnPreauth(listenerName string, cid int64, conn net.Conn, preauthAddress string) { + serve(listenerName, cid, nil, conn, false, true, false, preauthAddress) } // Serve starts serving on all listeners, launching a goroutine per listener. @@ -641,12 +645,26 @@ func (c *conn) xhighestModSeq(tx *bstore.Tx, mailboxID int64) store.ModSeq { var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection. -func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS, viaHTTPS bool) { +// serve handles a single IMAP connection on nc. +// +// If xtls is set, immediate TLS should be enabled on the connection, unless +// viaHTTP is set, which indicates TLS is already active with the connection coming +// from the webserver with IMAP chosen through ALPN. activated. If viaHTTP is set, +// the TLS config ddid not enable client certificate authentication. If xtls is +// false and tlsConfig is set, STARTTLS may enable TLS later on. +// +// If noRequireSTARTTLS is set, TLS is not required for authentication. +// +// If accountAddress is not empty, it is the email address of the account to open +// preauthenticated. +// +// The connection is closed before returning. +func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS, viaHTTPS bool, preauthAddress string) { var remoteIP net.IP if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok { remoteIP = a.IP } else { - // For net.Pipe, during tests. + // For net.Pipe, during tests and for imapserve. remoteIP = net.ParseIP("127.0.0.10") } @@ -768,6 +786,18 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x mox.Connections.Register(nc, "imap", listenerName) defer mox.Connections.Unregister(nc) + if preauthAddress != "" { + acc, _, err := store.OpenEmail(c.log, preauthAddress, false) + if err != nil { + c.log.Debugx("open account for preauth address", err, slog.String("address", preauthAddress)) + c.writelinef("* BYE open account for address: %s", err) + return + } + c.username = preauthAddress + c.account = acc + c.comm = store.RegisterComm(c.account) + } + if c.account != nil && !c.noPreauth { c.state = stateAuthenticated c.writelinef("* PREAUTH [CAPABILITY %s] mox imap welcomes %s", c.capabilities(), c.username) diff --git a/imapserver/server_test.go b/imapserver/server_test.go index 2f805db..66aa8b9 100644 --- a/imapserver/server_test.go +++ b/imapserver/server_test.go @@ -394,7 +394,7 @@ func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientC cid := connCounter go func() { const viaHTTPS = false - serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS) + serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS, "") if !noCloseSwitchboard { switchStop() } diff --git a/import.go b/import.go index fc630f0..4d4bad0 100644 --- a/import.go +++ b/import.go @@ -125,7 +125,7 @@ func xcmdXImport(mbox bool, c *cmd) { cconn, sconn := net.Pipe() clientctl := ctl{conn: cconn, r: bufio.NewReader(cconn), log: c.log} serverctl := ctl{conn: sconn, r: bufio.NewReader(sconn), log: c.log} - go servectlcmd(context.Background(), &serverctl, func() {}) + go servectlcmd(context.Background(), &serverctl, 0, func() {}) ctlcmdImport(&clientctl, mbox, account, args[1], args[2]) } diff --git a/localserve.go b/localserve.go index cb09381..de72125 100644 --- a/localserve.go +++ b/localserve.go @@ -218,7 +218,7 @@ during those commands instead of during "data". } cid := mox.Cid() ctx := context.WithValue(mox.Context, mlog.CidKey, cid) - go servectl(ctx, log.WithCid(cid), conn, func() { shutdown(log) }) + go servectl(ctx, cid, log.WithCid(cid), conn, func() { shutdown(log) }) } }() diff --git a/main.go b/main.go index b61760c..3b5a728 100644 --- a/main.go +++ b/main.go @@ -171,6 +171,8 @@ var commands = []struct { {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys}, {"config example", cmdConfigExample}, + {"admin imapserve", cmdIMAPServe}, + {"checkupdate", cmdCheckupdate}, {"cid", cmdCid}, {"clientconfig", cmdClientConfig}, @@ -3720,6 +3722,58 @@ func ctlcmdReassignthreads(ctl *ctl, account string) { ctl.xstreamto(os.Stdout) } +func cmdIMAPServe(c *cmd) { + c.params = "preauth-address" + c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0. + +For use with tools that can do IMAP over tunneled connections, e.g. with SSH +during migrations. TLS is not possible on the connection, and authentication +does not require TLS. +` + var fd0 bool + c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout") + args := c.Parse() + if len(args) != 1 { + c.Usage() + } + + address := args[0] + output := os.Stdout + if fd0 { + output = os.Stdout + } + ctlcmdIMAPServe(xctl(), address, os.Stdin, output) +} + +func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) { + ctl.xwrite("imapserve") + ctl.xwrite(address) + ctl.xreadok() + + done := make(chan struct{}, 1) + go func() { + defer func() { + done <- struct{}{} + }() + _, err := io.Copy(output, ctl.conn) + if err == nil { + err = io.EOF + } + log.Printf("reading from imap: %v", err) + }() + go func() { + defer func() { + done <- struct{}{} + }() + _, err := io.Copy(ctl.conn, input) + if err == nil { + err = io.EOF + } + log.Printf("writing to imap: %v", err) + }() + <-done +} + func cmdReadmessages(c *cmd) { c.unlisted = true c.params = "datadir account ..." diff --git a/serve_unix.go b/serve_unix.go index 31beffc..e46cd07 100644 --- a/serve_unix.go +++ b/serve_unix.go @@ -382,7 +382,7 @@ Only implemented on unix systems, not Windows. } cid := mox.Cid() ctx := context.WithValue(mox.Context, mlog.CidKey, cid) - go servectl(ctx, log.WithCid(cid), conn, func() { shutdown(log) }) + go servectl(ctx, cid, log.WithCid(cid), conn, func() { shutdown(log) }) } }()