Skip to content

Commit 49e2eba

Browse files
committed
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
1 parent 2d3d726 commit 49e2eba

File tree

11 files changed

+141
-14
lines changed

11 files changed

+141
-14
lines changed

ctl.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/mjl-/mox/admin"
2626
"github.com/mjl-/mox/config"
2727
"github.com/mjl-/mox/dns"
28+
"github.com/mjl-/mox/imapserver"
2829
"github.com/mjl-/mox/message"
2930
"github.com/mjl-/mox/metrics"
3031
"github.com/mjl-/mox/mlog"
@@ -277,7 +278,7 @@ func (s *ctlreader) xcheck(err error, msg string) {
277278
}
278279

279280
// servectl handles requests on the unix domain socket "ctl", e.g. for graceful shutdown, local mail delivery.
280-
func servectl(ctx context.Context, log mlog.Log, conn net.Conn, shutdown func()) {
281+
func servectl(ctx context.Context, cid int64, log mlog.Log, conn net.Conn, shutdown func()) {
281282
log.Debug("ctl connection")
282283

283284
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())
296297

297298
ctl.xwrite("ctlv0")
298299
for {
299-
servectlcmd(ctx, ctl, shutdown)
300+
servectlcmd(ctx, ctl, cid, shutdown)
300301
}
301302
}
302303

@@ -307,7 +308,7 @@ func xparseJSON(ctl *ctl, s string, v any) {
307308
ctl.xcheck(err, "parsing from ctl as json")
308309
}
309310

310-
func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
311+
func servectlcmd(ctx context.Context, ctl *ctl, cid int64, shutdown func()) {
311312
log := ctl.log
312313
cmd := ctl.xread()
313314
ctl.cmd = cmd
@@ -1824,6 +1825,18 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
18241825
case "backup":
18251826
backupctl(ctx, ctl)
18261827

1828+
case "imapserve":
1829+
/* protocol:
1830+
> "imapserve"
1831+
> address
1832+
< "ok or error"
1833+
imap protocol
1834+
*/
1835+
address := ctl.xread()
1836+
ctl.xwriteok()
1837+
imapserver.ServeConnPreauth("(imapserve)", cid, ctl.conn, address)
1838+
ctl.log.Debug("imap connection finished")
1839+
18271840
default:
18281841
log.Info("unrecognized command", slog.String("cmd", cmd))
18291842
ctl.xwrite("unrecognized command")

ctl_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/mjl-/mox/config"
2020
"github.com/mjl-/mox/dmarcdb"
2121
"github.com/mjl-/mox/dns"
22+
"github.com/mjl-/mox/imapclient"
2223
"github.com/mjl-/mox/mlog"
2324
"github.com/mjl-/mox/mox-"
2425
"github.com/mjl-/mox/mtastsdb"
@@ -58,6 +59,8 @@ func TestCtl(t *testing.T) {
5859
tcheck(t, err, "store init")
5960
defer store.Close()
6061

62+
var cid int64
63+
6164
testctl := func(fn func(clientctl *ctl)) {
6265
t.Helper()
6366

@@ -66,7 +69,8 @@ func TestCtl(t *testing.T) {
6669
serverctl := ctl{conn: sconn, log: pkglog}
6770
done := make(chan struct{})
6871
go func() {
69-
servectlcmd(ctxbg, &serverctl, func() {})
72+
cid++
73+
servectlcmd(ctxbg, &serverctl, cid, func() {})
7074
close(done)
7175
}()
7276
fn(&clientctl)
@@ -513,6 +517,19 @@ func TestCtl(t *testing.T) {
513517
flagArgs: []string{filepath.FromSlash("testdata/ctl/data/tmp/backup/data")},
514518
}
515519
cmdVerifydata(&xcmd)
520+
521+
// IMAP connection.
522+
testctl(func(ctl *ctl) {
523+
a, b := net.Pipe()
524+
go func() {
525+
client, err := imapclient.New(a, true)
526+
tcheck(t, err, "new imapclient")
527+
client.Select("inbox")
528+
client.Logout()
529+
defer a.Close()
530+
}()
531+
ctlcmdIMAPServe(ctl, "[email protected]", b, b)
532+
})
516533
}
517534

518535
func fakeCert(t *testing.T) []byte {

doc.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ any parameters. Followed by the help and usage information for each command.
8989
mox config printservice >mox.service
9090
mox config ensureacmehostprivatekeys
9191
mox config example [name]
92+
mox admin imapserve preauth-address
9293
mox checkupdate
9394
mox cid cid
9495
mox clientconfig domain
@@ -1204,6 +1205,18 @@ List available config examples, or print a specific example.
12041205
12051206
usage: mox config example [name]
12061207
1208+
# mox admin imapserve
1209+
1210+
Initiate a preauthenticated IMAP connection on file descriptor 0.
1211+
1212+
For use with tools that can do IMAP over tunneled connections, e.g. with SSH
1213+
during migrations. TLS is not possible on the connection, and authentication
1214+
does not require TLS.
1215+
1216+
usage: mox admin imapserve preauth-address
1217+
-fd0
1218+
write IMAP to file descriptor 0 instead of stdout
1219+
12071220
# mox checkupdate
12081221
12091222
Check if a newer version of mox is available.

imapserver/authenticate_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
368368
cid := connCounter
369369
go func() {
370370
defer serverConn.Close()
371-
serve("test", cid, &serverConfig, serverConn, true, false, false)
371+
serve("test", cid, &serverConfig, serverConn, true, false, false, "")
372372
close(done)
373373
}()
374374

imapserver/fuzz_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func FuzzServer(f *testing.F) {
133133

134134
err = serverConn.SetDeadline(time.Now().Add(time.Second))
135135
flog(err, "set server deadline")
136-
serve("test", cid, nil, serverConn, false, true, false)
136+
serve("test", cid, nil, serverConn, false, true, false, "")
137137
cid++
138138
}
139139

imapserver/server.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config,
381381
}
382382

383383
metricIMAPConnection.WithLabelValues(protocol).Inc()
384-
go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS, false)
384+
go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS, false, "")
385385
}
386386
}
387387

@@ -390,7 +390,11 @@ func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config,
390390

391391
// ServeTLSConn serves IMAP on a TLS connection.
392392
func ServeTLSConn(listenerName string, conn *tls.Conn, tlsConfig *tls.Config) {
393-
serve(listenerName, mox.Cid(), tlsConfig, conn, true, false, true)
393+
serve(listenerName, mox.Cid(), tlsConfig, conn, true, false, true, "")
394+
}
395+
396+
func ServeConnPreauth(listenerName string, cid int64, conn net.Conn, preauthAddress string) {
397+
serve(listenerName, cid, nil, conn, false, true, false, preauthAddress)
394398
}
395399

396400
// 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 {
641645

642646
var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
643647

644-
func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS, viaHTTPS bool) {
648+
// serve handles a single IMAP connection on nc.
649+
//
650+
// If xtls is set, immediate TLS should be enabled on the connection, unless
651+
// viaHTTP is set, which indicates TLS is already active with the connection coming
652+
// from the webserver with IMAP chosen through ALPN. activated. If viaHTTP is set,
653+
// the TLS config ddid not enable client certificate authentication. If xtls is
654+
// false and tlsConfig is set, STARTTLS may enable TLS later on.
655+
//
656+
// If noRequireSTARTTLS is set, TLS is not required for authentication.
657+
//
658+
// If accountAddress is not empty, it is the email address of the account to open
659+
// preauthenticated.
660+
//
661+
// The connection is closed before returning.
662+
func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS, viaHTTPS bool, preauthAddress string) {
645663
var remoteIP net.IP
646664
if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
647665
remoteIP = a.IP
648666
} else {
649-
// For net.Pipe, during tests.
667+
// For net.Pipe, during tests and for imapserve.
650668
remoteIP = net.ParseIP("127.0.0.10")
651669
}
652670

@@ -768,6 +786,18 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x
768786
mox.Connections.Register(nc, "imap", listenerName)
769787
defer mox.Connections.Unregister(nc)
770788

789+
if preauthAddress != "" {
790+
acc, _, err := store.OpenEmail(c.log, preauthAddress, false)
791+
if err != nil {
792+
c.log.Debugx("open account for preauth address", err, slog.String("address", preauthAddress))
793+
c.writelinef("* BYE open account for address: %s", err)
794+
return
795+
}
796+
c.username = preauthAddress
797+
c.account = acc
798+
c.comm = store.RegisterComm(c.account)
799+
}
800+
771801
if c.account != nil && !c.noPreauth {
772802
c.state = stateAuthenticated
773803
c.writelinef("* PREAUTH [CAPABILITY %s] mox imap welcomes %s", c.capabilities(), c.username)

imapserver/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientC
394394
cid := connCounter
395395
go func() {
396396
const viaHTTPS = false
397-
serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS)
397+
serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS, "")
398398
if !noCloseSwitchboard {
399399
switchStop()
400400
}

import.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func xcmdXImport(mbox bool, c *cmd) {
125125
cconn, sconn := net.Pipe()
126126
clientctl := ctl{conn: cconn, r: bufio.NewReader(cconn), log: c.log}
127127
serverctl := ctl{conn: sconn, r: bufio.NewReader(sconn), log: c.log}
128-
go servectlcmd(context.Background(), &serverctl, func() {})
128+
go servectlcmd(context.Background(), &serverctl, 0, func() {})
129129

130130
ctlcmdImport(&clientctl, mbox, account, args[1], args[2])
131131
}

localserve.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ during those commands instead of during "data".
218218
}
219219
cid := mox.Cid()
220220
ctx := context.WithValue(mox.Context, mlog.CidKey, cid)
221-
go servectl(ctx, log.WithCid(cid), conn, func() { shutdown(log) })
221+
go servectl(ctx, cid, log.WithCid(cid), conn, func() { shutdown(log) })
222222
}
223223
}()
224224

main.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ var commands = []struct {
171171
{"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
172172
{"config example", cmdConfigExample},
173173

174+
{"admin imapserve", cmdIMAPServe},
175+
174176
{"checkupdate", cmdCheckupdate},
175177
{"cid", cmdCid},
176178
{"clientconfig", cmdClientConfig},
@@ -3720,6 +3722,58 @@ func ctlcmdReassignthreads(ctl *ctl, account string) {
37203722
ctl.xstreamto(os.Stdout)
37213723
}
37223724

3725+
func cmdIMAPServe(c *cmd) {
3726+
c.params = "preauth-address"
3727+
c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0.
3728+
3729+
For use with tools that can do IMAP over tunneled connections, e.g. with SSH
3730+
during migrations. TLS is not possible on the connection, and authentication
3731+
does not require TLS.
3732+
`
3733+
var fd0 bool
3734+
c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout")
3735+
args := c.Parse()
3736+
if len(args) != 1 {
3737+
c.Usage()
3738+
}
3739+
3740+
address := args[0]
3741+
output := os.Stdout
3742+
if fd0 {
3743+
output = os.Stdout
3744+
}
3745+
ctlcmdIMAPServe(xctl(), address, os.Stdin, output)
3746+
}
3747+
3748+
func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) {
3749+
ctl.xwrite("imapserve")
3750+
ctl.xwrite(address)
3751+
ctl.xreadok()
3752+
3753+
done := make(chan struct{}, 1)
3754+
go func() {
3755+
defer func() {
3756+
done <- struct{}{}
3757+
}()
3758+
_, err := io.Copy(output, ctl.conn)
3759+
if err == nil {
3760+
err = io.EOF
3761+
}
3762+
log.Printf("reading from imap: %v", err)
3763+
}()
3764+
go func() {
3765+
defer func() {
3766+
done <- struct{}{}
3767+
}()
3768+
_, err := io.Copy(ctl.conn, input)
3769+
if err == nil {
3770+
err = io.EOF
3771+
}
3772+
log.Printf("writing to imap: %v", err)
3773+
}()
3774+
<-done
3775+
}
3776+
37233777
func cmdReadmessages(c *cmd) {
37243778
c.unlisted = true
37253779
c.params = "datadir account ..."

0 commit comments

Comments
 (0)