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) }) } }()