From c0eacfa0f34f4040ec4fd5d3ca44a35e8cde21b4 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Fri, 7 Jan 2022 00:37:29 +0300 Subject: [PATCH] Merge maddyctl and maddy executabes Closes #432. --- Dockerfile | 2 +- build.sh | 9 +- cmd/README.md | 7 - cmd/maddy/main.go | 8 +- cmd/maddyctl/imapacct.go | 136 --- cmd/maddyctl/main.go | 822 ------------------ cmd/maddyctl/users.go | 100 --- dist/systemd/maddy.service | 2 +- dist/systemd/maddy@.service | 2 +- docs/man/maddy.5.scd | 228 ----- framework/dns/debugflags.go | 10 +- internal/auth/pass_table/table.go | 17 +- internal/cli/app.go | 105 +++ .../cli}/clitools/clitools.go | 0 .../cli}/clitools/termios.go | 0 .../cli}/clitools/termios_stub.go | 0 .../cli/ctl}/appendlimit.go | 2 +- {cmd/maddyctl => internal/cli/ctl}/hash.go | 48 +- {cmd/maddyctl => internal/cli/ctl}/imap.go | 389 ++++++++- internal/cli/ctl/imapacct.go | 292 +++++++ internal/cli/ctl/moduleinit.go | 133 +++ internal/cli/ctl/users.go | 246 ++++++ internal/target/remote/debugflags.go | 11 +- maddy.go | 142 +-- tests/cover_test.go | 16 +- 25 files changed, 1343 insertions(+), 1384 deletions(-) delete mode 100644 cmd/maddyctl/imapacct.go delete mode 100644 cmd/maddyctl/main.go delete mode 100644 cmd/maddyctl/users.go delete mode 100644 docs/man/maddy.5.scd create mode 100644 internal/cli/app.go rename {cmd/maddyctl => internal/cli}/clitools/clitools.go (100%) rename {cmd/maddyctl => internal/cli}/clitools/termios.go (100%) rename {cmd/maddyctl => internal/cli}/clitools/termios_stub.go (100%) rename {cmd/maddyctl => internal/cli/ctl}/appendlimit.go (99%) rename {cmd/maddyctl => internal/cli/ctl}/hash.go (67%) rename {cmd/maddyctl => internal/cli/ctl}/imap.go (50%) create mode 100644 internal/cli/ctl/imapacct.go create mode 100644 internal/cli/ctl/moduleinit.go create mode 100644 internal/cli/ctl/users.go diff --git a/Dockerfile b/Dockerfile index 0d0f4ab..aa07b5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,4 +25,4 @@ COPY --from=build-env /pkg/usr/local/bin/maddyctl /bin/maddyctl EXPOSE 25 143 993 587 465 VOLUME ["/data"] -ENTRYPOINT ["/bin/maddy", "-config", "/data/maddy.conf"] +ENTRYPOINT ["/bin/maddy", "-config", "/data/maddy.conf", "run"] diff --git a/build.sh b/build.sh index 9c4e8cc..9ea3355 100755 --- a/build.sh +++ b/build.sh @@ -123,15 +123,9 @@ build() { go build -trimpath -buildmode pie -tags "$tags osusergo netgo static_build" \ -ldflags "-extldflags '-fno-PIC -static' -X \"github.com/foxcpp/maddy.Version=${version}\"" \ -o "${builddir}/maddy" ${GOFLAGS} ./cmd/maddy - echo "-- Building management utility (maddyctl)..." >&2 - go build -trimpath -buildmode pie -tags "$tags osusergo netgo static_build" \ - -ldflags "-extldflags '-fno-PIC -static' -X \"github.com/foxcpp/maddy.Version=${version}\"" \ - -o "${builddir}/maddyctl" ${GOFLAGS} ./cmd/maddyctl else echo "-- Building main server executable..." >&2 go build -tags "$tags" -trimpath -ldflags="-X \"github.com/foxcpp/maddy.Version=${version}\"" -o "${builddir}/maddy" ${GOFLAGS} ./cmd/maddy - echo "-- Building management utility (maddyctl)..." >&2 - go build -tags "$tags" -trimpath -ldflags="-X \"github.com/foxcpp/maddy.Version=${version}\"" -o "${builddir}/maddyctl" ${GOFLAGS} ./cmd/maddyctl fi build_man_pages @@ -147,7 +141,8 @@ install() { echo "-- Installing built files..." >&2 command install -m 0755 -d "${destdir}/${prefix}/bin/" - command install -m 0755 "${builddir}/maddy" "${builddir}/maddyctl" "${destdir}/${prefix}/bin/" + command install -m 0755 "${builddir}/maddy" "${destdir}/${prefix}/bin/" + command ln -s maddy "${destdir}/${prefix}/bin/maddyctl" command install -m 0755 -d "${destdir}/etc/maddy/" command install -m 0644 ./maddy.conf "${destdir}/etc/maddy/maddy.conf" diff --git a/cmd/README.md b/cmd/README.md index cff7ad6..3132fea 100644 --- a/cmd/README.md +++ b/cmd/README.md @@ -5,14 +5,7 @@ maddy executables Main server executable. -### maddyctl - -IMAP index and authentication database inspection and manipulation utility. - ### maddy-pam-helper, maddy-shadow-helper -__Deprecated: Currently they are unusable due to changes made to the storage -implementation.__ - Utilities compatible with the auth.external module that call libpam or read /etc/shadow on Unix systems. diff --git a/cmd/maddy/main.go b/cmd/maddy/main.go index 2d8047e..23c6047 100644 --- a/cmd/maddy/main.go +++ b/cmd/maddy/main.go @@ -19,11 +19,11 @@ along with this program. If not, see . package main import ( - "os" - - "github.com/foxcpp/maddy" + _ "github.com/foxcpp/maddy" + "github.com/foxcpp/maddy/internal/cli" + _ "github.com/foxcpp/maddy/internal/cli/ctl" ) func main() { - os.Exit(maddy.Run()) + maddycli.Run() } diff --git a/cmd/maddyctl/imapacct.go b/cmd/maddyctl/imapacct.go deleted file mode 100644 index 1b560e8..0000000 --- a/cmd/maddyctl/imapacct.go +++ /dev/null @@ -1,136 +0,0 @@ -/* -Maddy Mail Server - Composable all-in-one email server. -Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -package main - -import ( - "errors" - "fmt" - "os" - - "github.com/emersion/go-imap" - "github.com/foxcpp/maddy/cmd/maddyctl/clitools" - "github.com/foxcpp/maddy/framework/module" - "github.com/urfave/cli/v2" -) - -type SpecialUseUser interface { - CreateMailboxSpecial(name, specialUseAttr string) error -} - -func imapAcctList(be module.Storage, ctx *cli.Context) error { - mbe, ok := be.(module.ManageableStorage) - if !ok { - return cli.Exit("Error: storage backend does not support accounts management using maddyctl", 2) - } - - list, err := mbe.ListIMAPAccts() - if err != nil { - return err - } - - if len(list) == 0 && !ctx.Bool("quiet") { - fmt.Fprintln(os.Stderr, "No users.") - } - - for _, user := range list { - fmt.Println(user) - } - return nil -} - -func imapAcctCreate(be module.Storage, ctx *cli.Context) error { - mbe, ok := be.(module.ManageableStorage) - if !ok { - return cli.Exit("Error: storage backend does not support accounts management using maddyctl", 2) - } - - username := ctx.Args().First() - if username == "" { - return cli.Exit("Error: USERNAME is required", 2) - } - - if err := mbe.CreateIMAPAcct(username); err != nil { - return err - } - - act, err := mbe.GetIMAPAcct(username) - if err != nil { - return fmt.Errorf("failed to get user: %w", err) - } - - suu, ok := act.(SpecialUseUser) - if !ok { - fmt.Fprintf(os.Stderr, "Note: Storage backend does not support SPECIAL-USE IMAP extension") - } - - createMbox := func(name, specialUseAttr string) error { - if suu == nil { - return act.CreateMailbox(name) - } - return suu.CreateMailboxSpecial(name, specialUseAttr) - } - - if name := ctx.String("sent-name"); name != "" { - if err := createMbox(name, imap.SentAttr); err != nil { - fmt.Fprintf(os.Stderr, "Failed to create sent folder: %v", err) - } - } - if name := ctx.String("trash-name"); name != "" { - if err := createMbox(name, imap.TrashAttr); err != nil { - fmt.Fprintf(os.Stderr, "Failed to create trash folder: %v", err) - } - } - if name := ctx.String("junk-name"); name != "" { - if err := createMbox(name, imap.JunkAttr); err != nil { - fmt.Fprintf(os.Stderr, "Failed to create junk folder: %v", err) - } - } - if name := ctx.String("drafts-name"); name != "" { - if err := createMbox(name, imap.DraftsAttr); err != nil { - fmt.Fprintf(os.Stderr, "Failed to create drafts folder: %v", err) - } - } - if name := ctx.String("archive-name"); name != "" { - if err := createMbox(name, imap.ArchiveAttr); err != nil { - fmt.Fprintf(os.Stderr, "Failed to create archive folder: %v", err) - } - } - - return nil -} - -func imapAcctRemove(be module.Storage, ctx *cli.Context) error { - mbe, ok := be.(module.ManageableStorage) - if !ok { - return cli.Exit("Error: storage backend does not support accounts management using maddyctl", 2) - } - - username := ctx.Args().First() - if username == "" { - return cli.Exit("Error: USERNAME is required", 2) - } - - if !ctx.Bool("yes") { - if !clitools.Confirmation("Are you sure you want to delete this user account?", false) { - return errors.New("Cancelled") - } - } - - return mbe.DeleteIMAPAcct(username) -} diff --git a/cmd/maddyctl/main.go b/cmd/maddyctl/main.go deleted file mode 100644 index f784f88..0000000 --- a/cmd/maddyctl/main.go +++ /dev/null @@ -1,822 +0,0 @@ -/* -Maddy Mail Server - Composable all-in-one email server. -Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -package main - -import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" - "time" - - "github.com/foxcpp/maddy" - parser "github.com/foxcpp/maddy/framework/cfgparser" - "github.com/foxcpp/maddy/framework/config" - "github.com/foxcpp/maddy/framework/hooks" - "github.com/foxcpp/maddy/framework/module" - "github.com/foxcpp/maddy/internal/updatepipe" - "github.com/urfave/cli/v2" - "golang.org/x/crypto/bcrypt" -) - -func closeIfNeeded(i interface{}) { - if c, ok := i.(io.Closer); ok { - c.Close() - } -} - -func main() { - app := cli.NewApp() - app.Name = "maddyctl" - app.Usage = "maddy mail server administration utility" - app.Version = maddy.BuildInfo() - app.ExitErrHandler = func(c *cli.Context, err error) { - cli.HandleExitCoder(err) - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - cli.OsExiter(1) - } - } - app.Flags = []cli.Flag{ - &cli.PathFlag{ - Name: "config", - Usage: "Configuration file to use", - EnvVars: []string{"MADDY_CONFIG"}, - Value: filepath.Join(maddy.ConfigDirectory, "maddy.conf"), - }, - } - - app.Commands = []*cli.Command{ - { - Name: "creds", - Usage: "Local credentials management", - Subcommands: []*cli.Command{ - { - Name: "list", - Usage: "List created credentials", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_authdb", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openUserDB(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return usersList(be, ctx) - }, - }, - { - Name: "create", - Usage: "Create user account", - Description: "Reads password from stdin", - ArgsUsage: "USERNAME", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_authdb", - }, - &cli.StringFlag{ - Name: "password", - Aliases: []string{"p"}, - Usage: "Use `PASSWORD instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!", - }, - &cli.BoolFlag{ - Name: "null", - Aliases: []string{"n"}, - Usage: "Create account with null password", - }, - &cli.StringFlag{ - Name: "hash", - Usage: "Use specified hash algorithm. Valid values: sha3-512, bcrypt", - Value: "bcrypt", - }, - &cli.IntFlag{ - Name: "bcrypt-cost", - Usage: "Specify bcrypt cost value", - Value: bcrypt.DefaultCost, - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openUserDB(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return usersCreate(be, ctx) - }, - }, - { - Name: "remove", - Usage: "Delete user account", - ArgsUsage: "USERNAME", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_authdb", - }, - &cli.BoolFlag{ - Name: "yes", - Aliases: []string{"y"}, - Usage: "Don't ask for confirmation", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openUserDB(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return usersRemove(be, ctx) - }, - }, - { - Name: "password", - Usage: "Change account password", - Description: "Reads password from stdin", - ArgsUsage: "USERNAME", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_authdb", - }, - &cli.StringFlag{ - Name: "password", - Aliases: []string{"p"}, - Usage: "Use `PASSWORD` instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openUserDB(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return usersPassword(be, ctx) - }, - }, - }, - }, - { - Name: "imap-acct", - Usage: "IMAP storage accounts management", - Subcommands: []*cli.Command{ - { - Name: "list", - Usage: "List storage accounts", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return imapAcctList(be, ctx) - }, - }, - { - Name: "create", - Usage: "Create IMAP storage account", - ArgsUsage: "USERNAME", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.StringFlag{ - Name: "sent-name", - Usage: "Name of special mailbox for sent messages, use empty string to not create any", - Value: "Sent", - }, - &cli.StringFlag{ - Name: "trash-name", - Usage: "Name of special mailbox for trash, use empty string to not create any", - Value: "Trash", - }, - &cli.StringFlag{ - Name: "junk-name", - Usage: "Name of special mailbox for 'junk' (spam), use empty string to not create any", - Value: "Junk", - }, - &cli.StringFlag{ - Name: "drafts-name", - Usage: "Name of special mailbox for drafts, use empty string to not create any", - Value: "Drafts", - }, - &cli.StringFlag{ - Name: "archive-name", - Usage: "Name of special mailbox for archive, use empty string to not create any", - Value: "Archive", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return imapAcctCreate(be, ctx) - }, - }, - { - Name: "remove", - Usage: "Delete IMAP storage account", - ArgsUsage: "USERNAME", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "yes", - Aliases: []string{"y"}, - Usage: "Don't ask for confirmation", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return imapAcctRemove(be, ctx) - }, - }, - { - Name: "appendlimit", - Usage: "Query or set accounts's APPENDLIMIT value", - ArgsUsage: "USERNAME", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.IntFlag{ - Name: "value", - Aliases: []string{"v"}, - Usage: "Set APPENDLIMIT to specified value (in bytes)", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return imapAcctAppendlimit(be, ctx) - }, - }, - }, - }, - { - Name: "imap-mboxes", - Usage: "IMAP mailboxes (folders) management", - Subcommands: []*cli.Command{ - { - Name: "list", - Usage: "Show mailboxes of user", - ArgsUsage: "USERNAME", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "subscribed", - Aliases: []string{"s"}, - Usage: "List only subscribed mailboxes", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return mboxesList(be, ctx) - }, - }, - { - Name: "create", - Usage: "Create mailbox", - ArgsUsage: "USERNAME NAME", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.StringFlag{ - Name: "special", - Usage: "Set SPECIAL-USE attribute on mailbox; valid values: archive, drafts, junk, sent, trash", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return mboxesCreate(be, ctx) - }, - }, - { - Name: "remove", - Usage: "Remove mailbox", - Description: "WARNING: All contents of mailbox will be irrecoverably lost.", - ArgsUsage: "USERNAME MAILBOX", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "yes", - Aliases: []string{"y"}, - Usage: "Don't ask for confirmation", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return mboxesRemove(be, ctx) - }, - }, - { - Name: "rename", - Usage: "Rename mailbox", - Description: "Rename may cause unexpected failures on client-side so be careful.", - ArgsUsage: "USERNAME OLDNAME NEWNAME", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return mboxesRename(be, ctx) - }, - }, - }, - }, - { - Name: "imap-msgs", - Usage: "IMAP messages management", - Subcommands: []*cli.Command{ - { - Name: "add", - Usage: "Add message to mailbox", - ArgsUsage: "USERNAME MAILBOX", - Description: "Reads message body (with headers) from stdin. Prints UID of created message on success.", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.StringSliceFlag{ - Name: "flag", - Aliases: []string{"f"}, - Usage: "Add flag to message. Can be specified multiple times", - }, - &cli.TimestampFlag{ - Layout: time.RFC3339, - Name: "date", - Aliases: []string{"d"}, - Usage: "Set internal date value to specified one in ISO 8601 format (2006-01-02T15:04:05Z07:00)", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return msgsAdd(be, ctx) - }, - }, - { - Name: "add-flags", - Usage: "Add flags to messages", - ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...", - Description: "Add flags to all messages matched by SEQ.", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "uid", - Aliases: []string{"u"}, - Usage: "Use UIDs for SEQSET instead of sequence numbers", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return msgsFlags(be, ctx) - }, - }, - { - Name: "rem-flags", - Usage: "Remove flags from messages", - ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...", - Description: "Remove flags from all messages matched by SEQ.", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "uid", - Aliases: []string{"u"}, - Usage: "Use UIDs for SEQSET instead of sequence numbers", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return msgsFlags(be, ctx) - }, - }, - { - Name: "set-flags", - Usage: "Set flags on messages", - ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...", - Description: "Set flags on all messages matched by SEQ.", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "uid", - Aliases: []string{"u"}, - Usage: "Use UIDs for SEQSET instead of sequence numbers", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return msgsFlags(be, ctx) - }, - }, - { - Name: "remove", - Usage: "Remove messages from mailbox", - ArgsUsage: "USERNAME MAILBOX SEQSET", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "uid,u", - Aliases: []string{"u"}, - Usage: "Use UIDs for SEQSET instead of sequence numbers", - }, - &cli.BoolFlag{ - Name: "yes", - Aliases: []string{"y"}, - Usage: "Don't ask for confirmation", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return msgsRemove(be, ctx) - }, - }, - { - Name: "copy", - Usage: "Copy messages between mailboxes", - Description: "Note: You can't copy between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.", - ArgsUsage: "USERNAME SRCMAILBOX SEQSET TGTMAILBOX", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "uid", - Aliases: []string{"u"}, - Usage: "Use UIDs for SEQSET instead of sequence numbers", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return msgsCopy(be, ctx) - }, - }, - { - Name: "move", - Usage: "Move messages between mailboxes", - Description: "Note: You can't move between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.", - ArgsUsage: "USERNAME SRCMAILBOX SEQSET TGTMAILBOX", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "uid", - Aliases: []string{"u"}, - Usage: "Use UIDs for SEQSET instead of sequence numbers", - }, - &cli.BoolFlag{ - Name: "yes", - Aliases: []string{"y"}, - Usage: "Don't ask for confirmation", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return msgsMove(be, ctx) - }, - }, - { - Name: "list", - Usage: "List messages in mailbox", - Description: "If SEQSET is specified - only show messages that match it.", - ArgsUsage: "USERNAME MAILBOX [SEQSET]", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "uid", - Aliases: []string{"u"}, - Usage: "Use UIDs for SEQSET instead of sequence numbers", - }, - &cli.BoolFlag{ - Name: "full,f", - Aliases: []string{"f"}, - Usage: "Show entire envelope and all server meta-data", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return msgsList(be, ctx) - }, - }, - { - Name: "dump", - Usage: "Dump message body", - Description: "If passed SEQ matches multiple messages - they will be joined.", - ArgsUsage: "USERNAME MAILBOX SEQ", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cfg-block", - Usage: "Module configuration block to use", - EnvVars: []string{"MADDY_CFGBLOCK"}, - Value: "local_mailboxes", - }, - &cli.BoolFlag{ - Name: "uid", - Aliases: []string{"u"}, - Usage: "Use UIDs for SEQ instead of sequence numbers", - }, - }, - Action: func(ctx *cli.Context) error { - be, err := openStorage(ctx) - if err != nil { - return err - } - defer closeIfNeeded(be) - return msgsDump(be, ctx) - }, - }, - }, - }, - { - Name: "hash", - Usage: "Generate password hashes for use with pass_table", - Action: hashCommand, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "password", - Aliases: []string{"p"}, - Usage: "Use `PASSWORD instead of reading password from stdin\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!", - }, - &cli.StringFlag{ - Name: "hash", - Usage: "Use specified hash algorithm", - Value: "bcrypt", - }, - &cli.IntFlag{ - Name: "bcrypt-cost", - Usage: "Specify bcrypt cost value", - Value: bcrypt.DefaultCost, - }, - &cli.IntFlag{ - Name: "argon2-time", - Usage: "Time factor for Argon2id", - Value: 3, - }, - &cli.IntFlag{ - Name: "argon2-memory", - Usage: "Memory in KiB to use for Argon2id", - Value: 1024, - }, - &cli.IntFlag{ - Name: "argon2-threads", - Usage: "Threads to use for Argon2id", - Value: 1, - }, - }, - }, - } - - if err := app.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - } -} - -func getCfgBlockModule(ctx *cli.Context) (map[string]interface{}, *maddy.ModInfo, error) { - cfgPath := ctx.String("config") - if cfgPath == "" { - return nil, nil, cli.Exit("Error: config is required", 2) - } - cfgFile, err := os.Open(cfgPath) - if err != nil { - return nil, nil, cli.Exit(fmt.Sprintf("Error: failed to open config: %v", err), 2) - } - defer cfgFile.Close() - cfgNodes, err := parser.Read(cfgFile, cfgFile.Name()) - if err != nil { - return nil, nil, cli.Exit(fmt.Sprintf("Error: failed to parse config: %v", err), 2) - } - - globals, cfgNodes, err := maddy.ReadGlobals(cfgNodes) - if err != nil { - return nil, nil, err - } - - if err := maddy.InitDirs(); err != nil { - return nil, nil, err - } - - module.NoRun = true - _, mods, err := maddy.RegisterModules(globals, cfgNodes) - if err != nil { - return nil, nil, err - } - defer hooks.RunHooks(hooks.EventShutdown) - - cfgBlock := ctx.String("cfg-block") - if cfgBlock == "" { - return nil, nil, cli.Exit("Error: cfg-block is required", 2) - } - var mod maddy.ModInfo - for _, m := range mods { - if m.Instance.InstanceName() == cfgBlock { - mod = m - break - } - } - if mod.Instance == nil { - return nil, nil, cli.Exit(fmt.Sprintf("Error: unknown configuration block: %s", cfgBlock), 2) - } - - return globals, &mod, nil -} - -func openStorage(ctx *cli.Context) (module.Storage, error) { - globals, mod, err := getCfgBlockModule(ctx) - if err != nil { - return nil, err - } - - storage, ok := mod.Instance.(module.Storage) - if !ok { - return nil, cli.Exit(fmt.Sprintf("Error: configuration block %s is not an IMAP storage", ctx.String("cfg-block")), 2) - } - - if err := mod.Instance.Init(config.NewMap(globals, mod.Cfg)); err != nil { - return nil, fmt.Errorf("Error: module initialization failed: %w", err) - } - - if updStore, ok := mod.Instance.(updatepipe.Backend); ok { - if err := updStore.EnableUpdatePipe(updatepipe.ModePush); err != nil && !errors.Is(err, os.ErrNotExist) { - fmt.Fprintf(os.Stderr, "Failed to initialize update pipe, do not remove messages from mailboxes open by clients: %v\n", err) - } - } else { - fmt.Fprintf(os.Stderr, "No update pipe support, do not remove messages from mailboxes open by clients\n") - } - - return storage, nil -} - -func openUserDB(ctx *cli.Context) (module.PlainUserDB, error) { - globals, mod, err := getCfgBlockModule(ctx) - if err != nil { - return nil, err - } - - userDB, ok := mod.Instance.(module.PlainUserDB) - if !ok { - return nil, cli.Exit(fmt.Sprintf("Error: configuration block %s is not a local credentials store", ctx.String("cfg-block")), 2) - } - - if err := mod.Instance.Init(config.NewMap(globals, mod.Cfg)); err != nil { - return nil, fmt.Errorf("Error: module initialization failed: %w", err) - } - - return userDB, nil -} diff --git a/cmd/maddyctl/users.go b/cmd/maddyctl/users.go deleted file mode 100644 index b83d64c..0000000 --- a/cmd/maddyctl/users.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -Maddy Mail Server - Composable all-in-one email server. -Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -package main - -import ( - "errors" - "fmt" - "os" - - "github.com/foxcpp/maddy/cmd/maddyctl/clitools" - "github.com/foxcpp/maddy/framework/module" - "github.com/urfave/cli/v2" -) - -func usersList(be module.PlainUserDB, ctx *cli.Context) error { - list, err := be.ListUsers() - if err != nil { - return err - } - - if len(list) == 0 && !ctx.Bool("quiet") { - fmt.Fprintln(os.Stderr, "No users.") - } - - for _, user := range list { - fmt.Println(user) - } - return nil -} - -func usersCreate(be module.PlainUserDB, ctx *cli.Context) error { - username := ctx.Args().First() - if username == "" { - return cli.Exit("Error: USERNAME is required", 2) - } - - var pass string - if ctx.IsSet("password") { - pass = ctx.String("password") - } else { - var err error - pass, err = clitools.ReadPassword("Enter password for new user") - if err != nil { - return err - } - } - - return be.CreateUser(username, pass) -} - -func usersRemove(be module.PlainUserDB, ctx *cli.Context) error { - username := ctx.Args().First() - if username == "" { - return errors.New("Error: USERNAME is required") - } - - if !ctx.Bool("yes") { - if !clitools.Confirmation("Are you sure you want to delete this user account?", false) { - return errors.New("Cancelled") - } - } - - return be.DeleteUser(username) -} - -func usersPassword(be module.PlainUserDB, ctx *cli.Context) error { - username := ctx.Args().First() - if username == "" { - return errors.New("Error: USERNAME is required") - } - - var pass string - if ctx.IsSet("password") { - pass = ctx.String("password") - } else { - var err error - pass, err = clitools.ReadPassword("Enter new password") - if err != nil { - return err - } - } - - return be.SetUserPassword(username, pass) -} diff --git a/dist/systemd/maddy.service b/dist/systemd/maddy.service index 2377a9e..0f5ace2 100644 --- a/dist/systemd/maddy.service +++ b/dist/systemd/maddy.service @@ -72,7 +72,7 @@ Restart=on-failure # ... Unless it is a configuration problem. RestartPreventExitStatus=2 -ExecStart=/usr/local/bin/maddy +ExecStart=/usr/local/bin/maddy run ExecReload=/bin/kill -USR1 $MAINPID ExecReload=/bin/kill -USR2 $MAINPID diff --git a/dist/systemd/maddy@.service b/dist/systemd/maddy@.service index b056b6d..cc77682 100644 --- a/dist/systemd/maddy@.service +++ b/dist/systemd/maddy@.service @@ -68,7 +68,7 @@ Restart=on-failure # ... Unless it is a configuration problem. RestartPreventExitStatus=2 -ExecStart=/usr/local/bin/maddy -config /etc/maddy/%i.conf +ExecStart=/usr/local/bin/maddy --config /etc/maddy/%i.conf run ExecReload=/bin/kill -USR1 $MAINPID ExecReload=/bin/kill -USR2 $MAINPID diff --git a/docs/man/maddy.5.scd b/docs/man/maddy.5.scd deleted file mode 100644 index 06926b4..0000000 --- a/docs/man/maddy.5.scd +++ /dev/null @@ -1,228 +0,0 @@ -maddy(1) "maddy mail server" "maddy reference documentation" - -; TITLE Introduction - -# Modules - -maddy is built of many small components called "modules". Each module does one -certain well-defined task. Modules can be connected to each other in arbitrary -ways to achieve wanted functionality. Default configuration file defines -set of modules that together implement typical email server stack. - -To specify the module that should be used by another module for something, look -for configuration directives with "module reference" argument. Then -put the module name as an argument for it. Optionally, if referenced module -needs that, put additional arguments after the name. You can also put a -configuration block with additional directives specifing the module -configuration. - -Here are some examples: - -``` -smtp ... { - # Deliver messages to the 'dummy' module with the default configuration. - deliver_to dummy - - # Deliver messages to the 'target.smtp' module with - # 'tcp://127.0.0.1:1125' argument as a configuration. - deliver_to smtp tcp://127.0.0.1:1125 - - # Deliver messages to the 'queue' module with the specified configuration. - deliver_to queue { - target ... - max_tries 10 - } -} -``` - -Additionally, module configuration can be placed in a separate named block -at the top-level and merely referenced by its name where it is needed. - -Here is the example: -``` -storage.imapsql local_mailboxes { - driver sqlite3 - dsn all.db -} - -smtp ... { - deliver_to &local_mailboxes -} -``` - -It is recommended to use this syntax for modules that are 'expensive' to -initialize such as storage backends and authentication providers. - -For top-level configuration block definition, syntax is as follows: -``` -namespace.module_name config_block_name... { - module_configuration -} -``` -If config_block_name is omitted, it will be the same as module_name. Multiple -names can be specified. All names must be unique. - -Note the "storage." prefix. The actual module name is this and includes -"namespace". It is a little cheating to make more concise names and can -be omitted when you reference the module where it is used since it can -be implied (e.g. putting module reference in "check{}" likely means you want -something with "check." prefix) - -Usual module arguments can't be specified when using this syntax, however, -modules usually provide explicit directives that allow to specify the needed -values. For example 'sql sqlite3 all.db' is equivalent to -``` -storage.imapsql { - driver sqlite3 - dsn all.db -} -``` - -# Reference documentation conventions - -## Syntax descriptions for directives - -Underlined values are placeholders and should be replaced by your values. -_boolean_ is either 'yes' or 'no' string. - -Ellipsis (_smth..._) means that multiple values can be specified - -Multiple values listed with '|' (pipe) separator mean that any of them -can be used. - -# Global directives - -These directives applied for all configuration blocks that don't override it. - -*Syntax*: state_dir _path_ ++ -*Default*: /var/lib/maddy - -The path to the state directory. This directory will be used to store all -persistent data and should be writable. - -*Syntax*: runtime_dir _path_ ++ -*Default*: /run/maddy - -The path to the runtime directory. Used for Unix sockets and other temporary -objects. Should be writable. - -*Syntax*: hostname _domain_ ++ -*Default*: not specified - -Internet hostname of this mail server. Typicall FQDN is used. It is recommended -to make sure domain specified here resolved to the public IP of the server. - -*Syntax*: autogenerated_msg_domain _domain_ ++ -*Default*: not specified - -Domain that is used in From field for auto-generated messages (such as Delivery -Status Notifications). - -*Syntax*: ++ - tls file _cert_file_ _pkey_file_ ++ - tls _module reference_ ++ - tls off ++ -*Default*: not specified - -Default TLS certificate to use for all endpoints. - -Must be present in either all endpoint modules configuration blocks or as -global directive. - -You can also specify other configuration options such as cipher suites and TLS -version. See maddy-tls(5) for details. maddy uses reasonable -cipher suites and TLS versions by default so you generally don't have to worry -about it. - -*Syntax*: tls_client { ... } ++ -*Default*: not specified - -This is optional block that specifies various TLS-related options to use when -making outbound connections. See TLS client configuration for details on -directives that can be used in it. maddy uses reasonable cipher suites and TLS -versions by default so you generally don't have to worry about it. - -*Syntax*: ++ - log _targets..._ ++ - log off ++ -*Default*: stderr - -Write log to one of more "targets". - -The target can be one or the following: - -- stderr - - Write logs to stderr. - -- stderr_ts - - Write logs to stderr with timestamps. - -- syslog - - Send logs to the local syslog daemon. - -- _file path_ - - Write (append) logs to file. - -Example: -``` -log syslog /var/log/maddy.log -``` - -*Note:* Maddy does not perform log files rotation, this is the job of the -logrotate daemon. Send SIGUSR1 to maddy process to make it reopen log files. - -*Syntax*: debug _boolean_ ++ -*Default*: no - -Enable verbose logging for all modules. You don't need that unless you are -reporting a bug. - -# Prometheus/OpenMetrics endpoint - -``` -openmetrics tcp://127.0.0.1:9749 { } -``` - -This will enable HTTP listener that will serve telemetry in OpenMetrics format. -(It is compatible with Prometheus). - -See openmetrics.md documentation page the list of metrics exposed. - -# Signals - -*SIGTERM, SIGINT, SIGHUP* - -Stop the server process gracefully. Send the signal second time to force -immediate shutdown (likely unclean). - -*SIGUSR1* - -Reopen log files, if any are used. - -*SIGUSR2* - -Reload some files from disk, including alias mappings and TLS certificates. -This does not include the main configuration, though. - -# Authors - -Maintained by Max Mazurov . Project includes contributions -made by other people. - -Source code is available at https://github.com/foxcpp/maddy. - -# See also - -*maddy-config*(5) - Detailed configuration syntax description ++ -*maddy-imap*(5) - IMAP endpoint module reference ++ -*maddy-smtp*(5) - SMTP & Submission endpoint module reference ++ -*maddy-targets*(5) - Delivery targets reference ++ -*maddy-storage*(5) - Storage modules reference ++ -*maddy-auth*(5) - Authentication modules reference ++ -*maddy-filters*(5) - Message filtering modules reference ++ -*maddy-tables*(5) - Table modules reference ++ -*maddy-tls*(5) - Advanced TLS client & server configuration diff --git a/framework/dns/debugflags.go b/framework/dns/debugflags.go index 41e30fd..5fd0df4 100644 --- a/framework/dns/debugflags.go +++ b/framework/dns/debugflags.go @@ -21,9 +21,15 @@ along with this program. If not, see . package dns import ( - "flag" + maddycli "github.com/foxcpp/maddy/internal/cli" + "github.com/urfave/cli/v2" ) func init() { - flag.StringVar(&overrideServ, "debug.dnsoverride", "system-default", "replace the DNS resolver address") + maddycli.AddGlobalFlag(&cli.StringFlag{ + Name: "debug.dnsoverride", + Usage: "replace the DNS resolver address", + Value: "system-default", + Destination: &overrideServ, + }) } diff --git a/internal/auth/pass_table/table.go b/internal/auth/pass_table/table.go index 0bef271..5b9057f 100644 --- a/internal/auth/pass_table/table.go +++ b/internal/auth/pass_table/table.go @@ -112,11 +112,21 @@ func (a *Auth) ListUsers() ([]string, error) { } func (a *Auth) CreateUser(username, password string) error { + return a.CreateUserHash(username, password, HashBcrypt, HashOpts{ + BcryptCost: bcrypt.DefaultCost, + }) +} + +func (a *Auth) CreateUserHash(username, password string, hashAlgo string, opts HashOpts) error { tbl, ok := a.table.(module.MutableTable) if !ok { return fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName) } + if _, ok := HashCompute[hashAlgo]; !ok { + return fmt.Errorf("%s: unknown hash function: %v", a.modName, hashAlgo) + } + key, err := precis.UsernameCaseMapped.CompareKey(username) if err != nil { return fmt.Errorf("%s: create user %s (raw): %w", a.modName, username, err) @@ -130,15 +140,12 @@ func (a *Auth) CreateUser(username, password string) error { return fmt.Errorf("%s: credentials for %s already exist", a.modName, key) } - // TODO: Allow to customize hash function. - hash, err := HashCompute[HashBcrypt](HashOpts{ - BcryptCost: bcrypt.DefaultCost, - }, password) + hash, err := HashCompute[hashAlgo](opts, password) if err != nil { return fmt.Errorf("%s: create user %s: hash generation: %w", a.modName, key, err) } - if err := tbl.SetKey(key, "bcrypt:"+hash); err != nil { + if err := tbl.SetKey(key, hash+":"+hash); err != nil { return fmt.Errorf("%s: create user %s: %w", a.modName, key, err) } return nil diff --git a/internal/cli/app.go b/internal/cli/app.go new file mode 100644 index 0000000..78e6ec1 --- /dev/null +++ b/internal/cli/app.go @@ -0,0 +1,105 @@ +package maddycli + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/foxcpp/maddy/framework/log" + "github.com/urfave/cli/v2" +) + +var app *cli.App + +func init() { + app = cli.NewApp() + app.Usage = "composable all-in-one mail server" + app.Description = `Maddy is Mail Transfer agent (MTA), Mail Delivery Agent (MDA), Mail Submission +Agent (MSA), IMAP server and a set of other essential protocols/schemes +necessary to run secure email server implemented in one executable. + +This executable can be used to start the server ('run') and to manipulate +databases used by it (all other subcommands). +` + app.Authors = []*cli.Author{ + { + Name: "Maddy Mail Server maintainers & contributors", + Email: "~foxcpp/maddy@lists.sr.ht", + }, + } + app.ExitErrHandler = func(c *cli.Context, err error) { + cli.HandleExitCoder(err) + if err != nil { + log.Println(err) + cli.OsExiter(1) + } + } + app.EnableBashCompletion = true + app.Commands = []*cli.Command{ + { + Name: "generate-man", + Hidden: true, + Action: func(c *cli.Context) error { + man, err := app.ToMan() + if err != nil { + return err + } + fmt.Println(man) + return nil + }, + }, + { + Name: "generate-fish-completion", + Hidden: true, + Action: func(c *cli.Context) error { + cp, err := app.ToFishCompletion() + if err != nil { + return err + } + fmt.Println(cp) + return nil + }, + }, + } +} + +func AddGlobalFlag(f cli.Flag) { + app.Flags = append(app.Flags, f) + if err := f.Apply(flag.CommandLine); err != nil { + log.Println("GlobalFlag", f, "could not be mapped to stdlib flag:", err) + } +} + +func AddSubcommand(cmd *cli.Command) { + app.Commands = append(app.Commands, cmd) + + if cmd.Name == "run" { + // Backward compatibility hack to start the server as just ./maddy + // Needs to be done here so we will register all known flags with + // stdlib before Run is called. + app.Action = func(c *cli.Context) error { + log.Println("WARNING: Starting server not via 'maddy run' is deprecated and will stop working in the next version") + return cmd.Action(c) + } + app.Flags = append(app.Flags, cmd.Flags...) + for _, f := range cmd.Flags { + if err := f.Apply(flag.CommandLine); err != nil { + log.Println("GlobalFlag", f, "could not be mapped to stdlib flag:", err) + } + } + } +} + +func Run() { + // Actual entry point is registered in maddy.go. + + // Print help when called via maddyctl executable. To be removed + // once backward compatbility hack for 'maddy run' is removed too. + if strings.Contains(os.Args[0], "maddyctl") && len(os.Args) == 1 { + app.Run([]string{os.Args[0], "help"}) + return + } + + app.Run(os.Args) +} diff --git a/cmd/maddyctl/clitools/clitools.go b/internal/cli/clitools/clitools.go similarity index 100% rename from cmd/maddyctl/clitools/clitools.go rename to internal/cli/clitools/clitools.go diff --git a/cmd/maddyctl/clitools/termios.go b/internal/cli/clitools/termios.go similarity index 100% rename from cmd/maddyctl/clitools/termios.go rename to internal/cli/clitools/termios.go diff --git a/cmd/maddyctl/clitools/termios_stub.go b/internal/cli/clitools/termios_stub.go similarity index 100% rename from cmd/maddyctl/clitools/termios_stub.go rename to internal/cli/clitools/termios_stub.go diff --git a/cmd/maddyctl/appendlimit.go b/internal/cli/ctl/appendlimit.go similarity index 99% rename from cmd/maddyctl/appendlimit.go rename to internal/cli/ctl/appendlimit.go index 86a519d..ce0e3c4 100644 --- a/cmd/maddyctl/appendlimit.go +++ b/internal/cli/ctl/appendlimit.go @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -package main +package ctl import ( "fmt" diff --git a/cmd/maddyctl/hash.go b/internal/cli/ctl/hash.go similarity index 67% rename from cmd/maddyctl/hash.go rename to internal/cli/ctl/hash.go index 35a2b88..7ccc164 100644 --- a/cmd/maddyctl/hash.go +++ b/internal/cli/ctl/hash.go @@ -16,19 +16,61 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -package main +package ctl import ( "fmt" "os" "strings" - "github.com/foxcpp/maddy/cmd/maddyctl/clitools" "github.com/foxcpp/maddy/internal/auth/pass_table" + maddycli "github.com/foxcpp/maddy/internal/cli" + clitools2 "github.com/foxcpp/maddy/internal/cli/clitools" "github.com/urfave/cli/v2" "golang.org/x/crypto/bcrypt" ) +func init() { + maddycli.AddSubcommand( + &cli.Command{ + Name: "hash", + Usage: "Generate password hashes for use with pass_table", + Action: hashCommand, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "Use `PASSWORD instead of reading password from stdin\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!", + }, + &cli.StringFlag{ + Name: "hash", + Usage: "Use specified hash algorithm", + Value: "bcrypt", + }, + &cli.IntFlag{ + Name: "bcrypt-cost", + Usage: "Specify bcrypt cost value", + Value: bcrypt.DefaultCost, + }, + &cli.IntFlag{ + Name: "argon2-time", + Usage: "Time factor for Argon2id", + Value: 3, + }, + &cli.IntFlag{ + Name: "argon2-memory", + Usage: "Memory in KiB to use for Argon2id", + Value: 1024, + }, + &cli.IntFlag{ + Name: "argon2-threads", + Usage: "Threads to use for Argon2id", + Value: 1, + }, + }, + }) +} + func hashCommand(ctx *cli.Context) error { hashFunc := ctx.String("hash") if hashFunc == "" { @@ -75,7 +117,7 @@ func hashCommand(ctx *cli.Context) error { pass = ctx.String("password") } else { var err error - pass, err = clitools.ReadPassword("Password") + pass, err = clitools2.ReadPassword("Password") if err != nil { return err } diff --git a/cmd/maddyctl/imap.go b/internal/cli/ctl/imap.go similarity index 50% rename from cmd/maddyctl/imap.go rename to internal/cli/ctl/imap.go index 3681fc5..bc1de67 100644 --- a/cmd/maddyctl/imap.go +++ b/internal/cli/ctl/imap.go @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -package main +package ctl import ( "bytes" @@ -29,11 +29,386 @@ import ( "github.com/emersion/go-imap" imapsql "github.com/foxcpp/go-imap-sql" - "github.com/foxcpp/maddy/cmd/maddyctl/clitools" "github.com/foxcpp/maddy/framework/module" + maddycli "github.com/foxcpp/maddy/internal/cli" + clitools2 "github.com/foxcpp/maddy/internal/cli/clitools" "github.com/urfave/cli/v2" ) +func init() { + maddycli.AddSubcommand( + &cli.Command{ + Name: "imap-mboxes", + Usage: "IMAP mailboxes (folders) management", + Subcommands: []*cli.Command{ + { + Name: "list", + Usage: "Show mailboxes of user", + ArgsUsage: "USERNAME", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "subscribed", + Aliases: []string{"s"}, + Usage: "List only subscribed mailboxes", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return mboxesList(be, ctx) + }, + }, + { + Name: "create", + Usage: "Create mailbox", + ArgsUsage: "USERNAME NAME", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.StringFlag{ + Name: "special", + Usage: "Set SPECIAL-USE attribute on mailbox; valid values: archive, drafts, junk, sent, trash", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return mboxesCreate(be, ctx) + }, + }, + { + Name: "remove", + Usage: "Remove mailbox", + Description: "WARNING: All contents of mailbox will be irrecoverably lost.", + ArgsUsage: "USERNAME MAILBOX", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "yes", + Aliases: []string{"y"}, + Usage: "Don't ask for confirmation", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return mboxesRemove(be, ctx) + }, + }, + { + Name: "rename", + Usage: "Rename mailbox", + Description: "Rename may cause unexpected failures on client-side so be careful.", + ArgsUsage: "USERNAME OLDNAME NEWNAME", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return mboxesRename(be, ctx) + }, + }, + }, + }) + maddycli.AddSubcommand(&cli.Command{ + Name: "imap-msgs", + Usage: "IMAP messages management", + Subcommands: []*cli.Command{ + { + Name: "add", + Usage: "Add message to mailbox", + ArgsUsage: "USERNAME MAILBOX", + Description: "Reads message body (with headers) from stdin. Prints UID of created message on success.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.StringSliceFlag{ + Name: "flag", + Aliases: []string{"f"}, + Usage: "Add flag to message. Can be specified multiple times", + }, + &cli.TimestampFlag{ + Layout: time.RFC3339, + Name: "date", + Aliases: []string{"d"}, + Usage: "Set internal date value to specified one in ISO 8601 format (2006-01-02T15:04:05Z07:00)", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return msgsAdd(be, ctx) + }, + }, + { + Name: "add-flags", + Usage: "Add flags to messages", + ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...", + Description: "Add flags to all messages matched by SEQ.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "uid", + Aliases: []string{"u"}, + Usage: "Use UIDs for SEQSET instead of sequence numbers", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return msgsFlags(be, ctx) + }, + }, + { + Name: "rem-flags", + Usage: "Remove flags from messages", + ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...", + Description: "Remove flags from all messages matched by SEQ.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "uid", + Aliases: []string{"u"}, + Usage: "Use UIDs for SEQSET instead of sequence numbers", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return msgsFlags(be, ctx) + }, + }, + { + Name: "set-flags", + Usage: "Set flags on messages", + ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...", + Description: "Set flags on all messages matched by SEQ.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "uid", + Aliases: []string{"u"}, + Usage: "Use UIDs for SEQSET instead of sequence numbers", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return msgsFlags(be, ctx) + }, + }, + { + Name: "remove", + Usage: "Remove messages from mailbox", + ArgsUsage: "USERNAME MAILBOX SEQSET", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "uid,u", + Aliases: []string{"u"}, + Usage: "Use UIDs for SEQSET instead of sequence numbers", + }, + &cli.BoolFlag{ + Name: "yes", + Aliases: []string{"y"}, + Usage: "Don't ask for confirmation", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return msgsRemove(be, ctx) + }, + }, + { + Name: "copy", + Usage: "Copy messages between mailboxes", + Description: "Note: You can't copy between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.", + ArgsUsage: "USERNAME SRCMAILBOX SEQSET TGTMAILBOX", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "uid", + Aliases: []string{"u"}, + Usage: "Use UIDs for SEQSET instead of sequence numbers", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return msgsCopy(be, ctx) + }, + }, + { + Name: "move", + Usage: "Move messages between mailboxes", + Description: "Note: You can't move between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.", + ArgsUsage: "USERNAME SRCMAILBOX SEQSET TGTMAILBOX", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "uid", + Aliases: []string{"u"}, + Usage: "Use UIDs for SEQSET instead of sequence numbers", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return msgsMove(be, ctx) + }, + }, + { + Name: "list", + Usage: "List messages in mailbox", + Description: "If SEQSET is specified - only show messages that match it.", + ArgsUsage: "USERNAME MAILBOX [SEQSET]", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "uid", + Aliases: []string{"u"}, + Usage: "Use UIDs for SEQSET instead of sequence numbers", + }, + &cli.BoolFlag{ + Name: "full,f", + Aliases: []string{"f"}, + Usage: "Show entire envelope and all server meta-data", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return msgsList(be, ctx) + }, + }, + { + Name: "dump", + Usage: "Dump message body", + Description: "If passed SEQ matches multiple messages - they will be joined.", + ArgsUsage: "USERNAME MAILBOX SEQ", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "uid", + Aliases: []string{"u"}, + Usage: "Use UIDs for SEQ instead of sequence numbers", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return msgsDump(be, ctx) + }, + }, + }, + }) +} + func FormatAddress(addr *imap.Address) string { return fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName) } @@ -131,7 +506,7 @@ func mboxesRemove(be module.Storage, ctx *cli.Context) error { fmt.Fprintf(os.Stderr, "Mailbox %s contains %d messages.\n", name, status.Messages) } - if !clitools.Confirmation("Are you sure you want to delete that mailbox?", false) { + if !clitools2.Confirmation("Are you sure you want to delete that mailbox?", false) { return errors.New("Cancelled") } } @@ -183,7 +558,7 @@ func msgsAdd(be module.Storage, ctx *cli.Context) error { date := time.Now() if ctx.IsSet("date") { - date = time.Unix(ctx.Int64("date"), 0) + date = *ctx.Timestamp("date") } buf := bytes.Buffer{} @@ -240,7 +615,7 @@ func msgsRemove(be module.Storage, ctx *cli.Context) error { } if !ctx.Bool("yes") { - if !clitools.Confirmation("Are you sure you want to delete these messages?", false) { + if !clitools2.Confirmation("Are you sure you want to delete these messages?", false) { return errors.New("Cancelled") } } @@ -286,10 +661,6 @@ func msgsCopy(be module.Storage, ctx *cli.Context) error { } func msgsMove(be module.Storage, ctx *cli.Context) error { - if ctx.Bool("yes") || !clitools.Confirmation("Currently, it is unsafe to remove messages from mailboxes used by connected clients, continue?", false) { - return cli.Exit("Cancelled", 2) - } - username := ctx.Args().First() if username == "" { return cli.Exit("Error: USERNAME is required", 2) diff --git a/internal/cli/ctl/imapacct.go b/internal/cli/ctl/imapacct.go new file mode 100644 index 0000000..0be8e4d --- /dev/null +++ b/internal/cli/ctl/imapacct.go @@ -0,0 +1,292 @@ +/* +Maddy Mail Server - Composable all-in-one email server. +Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package ctl + +import ( + "errors" + "fmt" + "os" + + "github.com/emersion/go-imap" + "github.com/foxcpp/maddy/framework/module" + maddycli "github.com/foxcpp/maddy/internal/cli" + clitools2 "github.com/foxcpp/maddy/internal/cli/clitools" + "github.com/urfave/cli/v2" +) + +func init() { + maddycli.AddSubcommand( + &cli.Command{ + Name: "imap-acct", + Usage: "IMAP storage accounts management", + Description: `These subcommands can be used to list/create/delete IMAP storage +accounts for any storage backend supported by maddy. + +The corresponding storage backend should be configured in maddy.conf and be +defined in a top-level configuration block. By default, the name of that +block should be local_mailboxes but this can be changed using --cfg-block +flag for subcommands. + +Note that in default configuration it is not enough to create an IMAP storage +account to grant server access. Additionally, user credentials should +be created using 'creds' subcommand. +`, + Subcommands: []*cli.Command{ + { + Name: "list", + Usage: "List storage accounts", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return imapAcctList(be, ctx) + }, + }, + { + Name: "create", + Usage: "Create IMAP storage account", + Description: `In addition to account creation, this command +creates a set of default folder (mailboxes) with special-use attribute set.`, + ArgsUsage: "USERNAME", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.StringFlag{ + Name: "sent-name", + Usage: "Name of special mailbox for sent messages, use empty string to not create any", + Value: "Sent", + }, + &cli.StringFlag{ + Name: "trash-name", + Usage: "Name of special mailbox for trash, use empty string to not create any", + Value: "Trash", + }, + &cli.StringFlag{ + Name: "junk-name", + Usage: "Name of special mailbox for 'junk' (spam), use empty string to not create any", + Value: "Junk", + }, + &cli.StringFlag{ + Name: "drafts-name", + Usage: "Name of special mailbox for drafts, use empty string to not create any", + Value: "Drafts", + }, + &cli.StringFlag{ + Name: "archive-name", + Usage: "Name of special mailbox for archive, use empty string to not create any", + Value: "Archive", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return imapAcctCreate(be, ctx) + }, + }, + { + Name: "remove", + Usage: "Delete IMAP storage account", + Description: `If IMAP connections are open and using the specified account, +messages access will be killed off immediately though connection will remain open. No cache +or other buffering takes effect.`, + ArgsUsage: "USERNAME", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.BoolFlag{ + Name: "yes", + Aliases: []string{"y"}, + Usage: "Don't ask for confirmation", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return imapAcctRemove(be, ctx) + }, + }, + { + Name: "appendlimit", + Usage: "Query or set accounts's APPENDLIMIT value", + Description: `APPENDLIMIT value determines the size of a message that +can be saved into a mailbox using IMAP APPEND command. This does not affect the size +of messages that can be delivered to the mailbox from non-IMAP sources (e.g. SMTP). + +Global APPENDLIMIT value set via server configuration takes precedence over +per-account values configured using this command. + +APPENDLIMIT value (either global or per-account) cannot be larger than +4 GiB due to IMAP protocol limitations. +`, + ArgsUsage: "USERNAME", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_mailboxes", + }, + &cli.IntFlag{ + Name: "value", + Aliases: []string{"v"}, + Usage: "Set APPENDLIMIT to specified value (in bytes)", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openStorage(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return imapAcctAppendlimit(be, ctx) + }, + }, + }, + }) +} + +type SpecialUseUser interface { + CreateMailboxSpecial(name, specialUseAttr string) error +} + +func imapAcctList(be module.Storage, ctx *cli.Context) error { + mbe, ok := be.(module.ManageableStorage) + if !ok { + return cli.Exit("Error: storage backend does not support accounts management using maddyctl", 2) + } + + list, err := mbe.ListIMAPAccts() + if err != nil { + return err + } + + if len(list) == 0 && !ctx.Bool("quiet") { + fmt.Fprintln(os.Stderr, "No users.") + } + + for _, user := range list { + fmt.Println(user) + } + return nil +} + +func imapAcctCreate(be module.Storage, ctx *cli.Context) error { + mbe, ok := be.(module.ManageableStorage) + if !ok { + return cli.Exit("Error: storage backend does not support accounts management using maddyctl", 2) + } + + username := ctx.Args().First() + if username == "" { + return cli.Exit("Error: USERNAME is required", 2) + } + + if err := mbe.CreateIMAPAcct(username); err != nil { + return err + } + + act, err := mbe.GetIMAPAcct(username) + if err != nil { + return fmt.Errorf("failed to get user: %w", err) + } + + suu, ok := act.(SpecialUseUser) + if !ok { + fmt.Fprintf(os.Stderr, "Note: Storage backend does not support SPECIAL-USE IMAP extension") + } + + createMbox := func(name, specialUseAttr string) error { + if suu == nil { + return act.CreateMailbox(name) + } + return suu.CreateMailboxSpecial(name, specialUseAttr) + } + + if name := ctx.String("sent-name"); name != "" { + if err := createMbox(name, imap.SentAttr); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create sent folder: %v", err) + } + } + if name := ctx.String("trash-name"); name != "" { + if err := createMbox(name, imap.TrashAttr); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create trash folder: %v", err) + } + } + if name := ctx.String("junk-name"); name != "" { + if err := createMbox(name, imap.JunkAttr); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create junk folder: %v", err) + } + } + if name := ctx.String("drafts-name"); name != "" { + if err := createMbox(name, imap.DraftsAttr); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create drafts folder: %v", err) + } + } + if name := ctx.String("archive-name"); name != "" { + if err := createMbox(name, imap.ArchiveAttr); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create archive folder: %v", err) + } + } + + return nil +} + +func imapAcctRemove(be module.Storage, ctx *cli.Context) error { + mbe, ok := be.(module.ManageableStorage) + if !ok { + return cli.Exit("Error: storage backend does not support accounts management using maddyctl", 2) + } + + username := ctx.Args().First() + if username == "" { + return cli.Exit("Error: USERNAME is required", 2) + } + + if !ctx.Bool("yes") { + if !clitools2.Confirmation("Are you sure you want to delete this user account?", false) { + return errors.New("Cancelled") + } + } + + return mbe.DeleteIMAPAcct(username) +} diff --git a/internal/cli/ctl/moduleinit.go b/internal/cli/ctl/moduleinit.go new file mode 100644 index 0000000..23e79e5 --- /dev/null +++ b/internal/cli/ctl/moduleinit.go @@ -0,0 +1,133 @@ +/* +Maddy Mail Server - Composable all-in-one email server. +Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package ctl + +import ( + "errors" + "fmt" + "io" + "os" + + "github.com/foxcpp/maddy" + parser "github.com/foxcpp/maddy/framework/cfgparser" + "github.com/foxcpp/maddy/framework/config" + "github.com/foxcpp/maddy/framework/hooks" + "github.com/foxcpp/maddy/framework/module" + "github.com/foxcpp/maddy/internal/updatepipe" + "github.com/urfave/cli/v2" +) + +func closeIfNeeded(i interface{}) { + if c, ok := i.(io.Closer); ok { + c.Close() + } +} + +func getCfgBlockModule(ctx *cli.Context) (map[string]interface{}, *maddy.ModInfo, error) { + cfgPath := ctx.String("config") + if cfgPath == "" { + return nil, nil, cli.Exit("Error: config is required", 2) + } + cfgFile, err := os.Open(cfgPath) + if err != nil { + return nil, nil, cli.Exit(fmt.Sprintf("Error: failed to open config: %v", err), 2) + } + defer cfgFile.Close() + cfgNodes, err := parser.Read(cfgFile, cfgFile.Name()) + if err != nil { + return nil, nil, cli.Exit(fmt.Sprintf("Error: failed to parse config: %v", err), 2) + } + + globals, cfgNodes, err := maddy.ReadGlobals(cfgNodes) + if err != nil { + return nil, nil, err + } + + if err := maddy.InitDirs(); err != nil { + return nil, nil, err + } + + module.NoRun = true + _, mods, err := maddy.RegisterModules(globals, cfgNodes) + if err != nil { + return nil, nil, err + } + defer hooks.RunHooks(hooks.EventShutdown) + + cfgBlock := ctx.String("cfg-block") + if cfgBlock == "" { + return nil, nil, cli.Exit("Error: cfg-block is required", 2) + } + var mod maddy.ModInfo + for _, m := range mods { + if m.Instance.InstanceName() == cfgBlock { + mod = m + break + } + } + if mod.Instance == nil { + return nil, nil, cli.Exit(fmt.Sprintf("Error: unknown configuration block: %s", cfgBlock), 2) + } + + return globals, &mod, nil +} + +func openStorage(ctx *cli.Context) (module.Storage, error) { + globals, mod, err := getCfgBlockModule(ctx) + if err != nil { + return nil, err + } + + storage, ok := mod.Instance.(module.Storage) + if !ok { + return nil, cli.Exit(fmt.Sprintf("Error: configuration block %s is not an IMAP storage", ctx.String("cfg-block")), 2) + } + + if err := mod.Instance.Init(config.NewMap(globals, mod.Cfg)); err != nil { + return nil, fmt.Errorf("Error: module initialization failed: %w", err) + } + + if updStore, ok := mod.Instance.(updatepipe.Backend); ok { + if err := updStore.EnableUpdatePipe(updatepipe.ModePush); err != nil && !errors.Is(err, os.ErrNotExist) { + fmt.Fprintf(os.Stderr, "Failed to initialize update pipe, do not remove messages from mailboxes open by clients: %v\n", err) + } + } else { + fmt.Fprintf(os.Stderr, "No update pipe support, do not remove messages from mailboxes open by clients\n") + } + + return storage, nil +} + +func openUserDB(ctx *cli.Context) (module.PlainUserDB, error) { + globals, mod, err := getCfgBlockModule(ctx) + if err != nil { + return nil, err + } + + userDB, ok := mod.Instance.(module.PlainUserDB) + if !ok { + return nil, cli.Exit(fmt.Sprintf("Error: configuration block %s is not a local credentials store", ctx.String("cfg-block")), 2) + } + + if err := mod.Instance.Init(config.NewMap(globals, mod.Cfg)); err != nil { + return nil, fmt.Errorf("Error: module initialization failed: %w", err) + } + + return userDB, nil +} diff --git a/internal/cli/ctl/users.go b/internal/cli/ctl/users.go new file mode 100644 index 0000000..924e433 --- /dev/null +++ b/internal/cli/ctl/users.go @@ -0,0 +1,246 @@ +/* +Maddy Mail Server - Composable all-in-one email server. +Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package ctl + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/foxcpp/maddy/framework/module" + "github.com/foxcpp/maddy/internal/auth/pass_table" + maddycli "github.com/foxcpp/maddy/internal/cli" + clitools2 "github.com/foxcpp/maddy/internal/cli/clitools" + "github.com/urfave/cli/v2" + "golang.org/x/crypto/bcrypt" +) + +func init() { + maddycli.AddSubcommand( + &cli.Command{ + Name: "creds", + Usage: "Local credentials management", + Description: `These commands manipulate credential databases used by +maddy mail server. + +Corresponding credential database should be defined in maddy.conf as +a top-level config block. By default the block name should be local_authdb ( +can be changed using --cfg-block argument for subcommands). + +Note that it is not enough to create user credentials in order to grant +IMAP access - IMAP account should be also created using 'imap-acct create' subcommand. +`, + Subcommands: []*cli.Command{ + { + Name: "list", + Usage: "List created credentials", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_authdb", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openUserDB(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return usersList(be, ctx) + }, + }, + { + Name: "create", + Usage: "Create user account", + Description: `Reads password from stdin. + +If configuration block uses auth.pass_table, then hash algorithm can be configured +using command flags. Otherwise, these options cannot be used. +`, + ArgsUsage: "USERNAME", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_authdb", + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "Use `PASSWORD instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!", + }, + &cli.StringFlag{ + Name: "hash", + Usage: "Use specified hash algorithm. Valid values: " + strings.Join(pass_table.Hashes, ", "), + Value: "bcrypt", + }, + &cli.IntFlag{ + Name: "bcrypt-cost", + Usage: "Specify bcrypt cost value", + Value: bcrypt.DefaultCost, + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openUserDB(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return usersCreate(be, ctx) + }, + }, + { + Name: "remove", + Usage: "Delete user account", + ArgsUsage: "USERNAME", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_authdb", + }, + &cli.BoolFlag{ + Name: "yes", + Aliases: []string{"y"}, + Usage: "Don't ask for confirmation", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openUserDB(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return usersRemove(be, ctx) + }, + }, + { + Name: "password", + Usage: "Change account password", + Description: "Reads password from stdin", + ArgsUsage: "USERNAME", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cfg-block", + Usage: "Module configuration block to use", + EnvVars: []string{"MADDY_CFGBLOCK"}, + Value: "local_authdb", + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "Use `PASSWORD` instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!", + }, + }, + Action: func(ctx *cli.Context) error { + be, err := openUserDB(ctx) + if err != nil { + return err + } + defer closeIfNeeded(be) + return usersPassword(be, ctx) + }, + }, + }, + }) +} + +func usersList(be module.PlainUserDB, ctx *cli.Context) error { + list, err := be.ListUsers() + if err != nil { + return err + } + + if len(list) == 0 && !ctx.Bool("quiet") { + fmt.Fprintln(os.Stderr, "No users.") + } + + for _, user := range list { + fmt.Println(user) + } + return nil +} + +func usersCreate(be module.PlainUserDB, ctx *cli.Context) error { + username := ctx.Args().First() + if username == "" { + return cli.Exit("Error: USERNAME is required", 2) + } + + var pass string + if ctx.IsSet("password") { + pass = ctx.String("password") + } else { + var err error + pass, err = clitools2.ReadPassword("Enter password for new user") + if err != nil { + return err + } + } + + if be, ok := be.(*pass_table.Auth); ok { + return be.CreateUserHash(username, pass, ctx.String("hash"), pass_table.HashOpts{ + BcryptCost: ctx.Int("bcrypt-cost"), + }) + } else if ctx.IsSet("hash") || ctx.IsSet("bcrypt-cost") { + return cli.Exit("Error: --hash cannot be used with non-pass_table credentials DB", 2) + } else { + return be.CreateUser(username, pass) + } +} + +func usersRemove(be module.PlainUserDB, ctx *cli.Context) error { + username := ctx.Args().First() + if username == "" { + return errors.New("Error: USERNAME is required") + } + + if !ctx.Bool("yes") { + if !clitools2.Confirmation("Are you sure you want to delete this user account?", false) { + return errors.New("Cancelled") + } + } + + return be.DeleteUser(username) +} + +func usersPassword(be module.PlainUserDB, ctx *cli.Context) error { + username := ctx.Args().First() + if username == "" { + return errors.New("Error: USERNAME is required") + } + + var pass string + if ctx.IsSet("password") { + pass = ctx.String("password") + } else { + var err error + pass, err = clitools2.ReadPassword("Enter new password") + if err != nil { + return err + } + } + + return be.SetUserPassword(username, pass) +} diff --git a/internal/target/remote/debugflags.go b/internal/target/remote/debugflags.go index c79d931..76774b9 100644 --- a/internal/target/remote/debugflags.go +++ b/internal/target/remote/debugflags.go @@ -20,8 +20,15 @@ along with this program. If not, see . package remote -import "flag" +import ( + maddycli "github.com/foxcpp/maddy/internal/cli" + "github.com/urfave/cli/v2" +) func init() { - flag.StringVar(&smtpPort, "debug.smtpport", "25", "SMTP port to use for connections in tests") + maddycli.AddGlobalFlag(&cli.StringFlag{ + Name: "debug.smtpport", + Usage: "SMTP port to use for connections in tests", + Destination: &smtpPort, + }) } diff --git a/maddy.go b/maddy.go index 43df3db..d931814 100644 --- a/maddy.go +++ b/maddy.go @@ -20,7 +20,6 @@ package maddy import ( "errors" - "flag" "fmt" "io" "net/http" @@ -28,7 +27,6 @@ import ( "path/filepath" "runtime" "runtime/debug" - "strings" "github.com/caddyserver/certmagic" parser "github.com/foxcpp/maddy/framework/cfgparser" @@ -37,6 +35,8 @@ import ( "github.com/foxcpp/maddy/framework/hooks" "github.com/foxcpp/maddy/framework/log" "github.com/foxcpp/maddy/framework/module" + maddycli "github.com/foxcpp/maddy/internal/cli" + "github.com/urfave/cli/v2" // Import packages for side-effect of module registration. _ "github.com/foxcpp/maddy/internal/auth/dovecot_sasl" @@ -78,10 +78,7 @@ import ( var ( Version = "go-build" - enableDebugFlags = false - profileEndpoint *string - blockProfileRate *int - mutexProfileFract *int + enableDebugFlags = false ) func BuildInfo() string { @@ -101,94 +98,135 @@ default runtime_dir: %s`, DefaultRuntimeDirectory) } -// Run is the entry point for all maddy code. It takes care of command line arguments parsing, -// logging initialization, directives setup, configuration reading. After all that, it -// calls moduleMain to initialize and run modules. -func Run() int { - certmagic.UserAgent = "maddy/" + Version - - flag.StringVar(&config.LibexecDirectory, "libexec", DefaultLibexecDirectory, "path to the libexec directory") - flag.BoolVar(&log.DefaultLogger.Debug, "debug", false, "enable debug logging early") - - var ( - configPath = flag.String("config", filepath.Join(ConfigDirectory, "maddy.conf"), "path to configuration file") - logTargets = flag.String("log", "stderr", "default logging target(s)") - printVersion = flag.Bool("v", false, "print version and build metadata, then exit") +func init() { + maddycli.AddGlobalFlag( + &cli.PathFlag{ + Name: "config", + Usage: "Configuration file to use", + EnvVars: []string{"MADDY_CONFIG"}, + Value: filepath.Join(ConfigDirectory, "maddy.conf"), + }, ) + maddycli.AddSubcommand(&cli.Command{ + Name: "run", + Usage: "Start the server", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "enable debug logging early", + Destination: &log.DefaultLogger.Debug, + }, + &cli.StringFlag{ + Name: "libexec", + Value: DefaultLibexecDirectory, + Usage: "path to the libexec directory", + Destination: &config.LibexecDirectory, + }, + &cli.StringSliceFlag{ + Name: "log", + Usage: "default logging target(s)", + Value: cli.NewStringSlice("stderr"), + }, + &cli.BoolFlag{ + Name: "v", + Usage: "print version and build metadata, then exit", + Hidden: true, + }, + }, + Action: Run, + }) + maddycli.AddSubcommand(&cli.Command{ + Name: "version", + Usage: "Print version and build metadata, then exit", + Action: func(c *cli.Context) error { + fmt.Println(BuildInfo()) + return nil + }, + }) if enableDebugFlags { - profileEndpoint = flag.String("debug.pprof", "", "enable live profiler HTTP endpoint and listen on the specified address") - blockProfileRate = flag.Int("debug.blockprofrate", 0, "set blocking profile rate") - mutexProfileFract = flag.Int("debug.mutexproffract", 0, "set mutex profile fraction") + maddycli.AddGlobalFlag(&cli.StringFlag{ + Name: "debug.pprof", + Usage: "enable live profiler HTTP endpoint and listen on the specified address", + }) + maddycli.AddGlobalFlag(&cli.IntFlag{ + Name: "debug.blockprofrate", + Usage: "set blocking profile rate", + }) + maddycli.AddGlobalFlag(&cli.IntFlag{ + Name: "debug.mutexproffract", + Usage: "set mutex profile fraction", + }) + } +} + +// Run is the entry point for all server-running code. It takes care of command line arguments processing, +// logging initialization, directives setup, configuration reading. After all that, it +// calls moduleMain to initialize and run modules. +func Run(c *cli.Context) error { + certmagic.UserAgent = "maddy/" + Version + + if c.NArg() != 0 { + return cli.Exit(fmt.Sprintln("usage:", os.Args[0], "[options]"), 2) } - flag.Parse() - - if len(flag.Args()) != 0 { - fmt.Println("usage:", os.Args[0], "[options]") - return 2 - } - - if *printVersion { + if c.Bool("v") { fmt.Println("maddy", BuildInfo()) - return 0 + return nil } var err error - log.DefaultLogger.Out, err = LogOutputOption(strings.Split(*logTargets, ",")) + log.DefaultLogger.Out, err = LogOutputOption(c.StringSlice("log")) if err != nil { systemdStatusErr(err) - log.Println(err) - return 2 + return cli.Exit(err.Error(), 2) } - initDebug() + initDebug(c) os.Setenv("PATH", config.LibexecDirectory+string(filepath.ListSeparator)+os.Getenv("PATH")) - f, err := os.Open(*configPath) + f, err := os.Open(c.Path("config")) if err != nil { systemdStatusErr(err) - log.Println(err) - return 2 + return cli.Exit(err.Error(), 2) } defer f.Close() - cfg, err := parser.Read(f, *configPath) + cfg, err := parser.Read(f, c.Path("config")) if err != nil { systemdStatusErr(err) - log.Println(err) - return 2 + return cli.Exit(err.Error(), 2) } if err := moduleMain(cfg); err != nil { systemdStatusErr(err) - log.Println(err) - return 2 + return cli.Exit(err.Error(), 1) } - return 0 + return nil } -func initDebug() { +func initDebug(c *cli.Context) { if !enableDebugFlags { return } - if *profileEndpoint != "" { + if c.IsSet("debug.pprof") { + profileEndpoint := c.String("debug.pprof") go func() { - log.Println("listening on", "http://"+*profileEndpoint, "for profiler requests") - log.Println("failed to listen on profiler endpoint:", http.ListenAndServe(*profileEndpoint, nil)) + log.Println("listening on", "http://"+profileEndpoint, "for profiler requests") + log.Println("failed to listen on profiler endpoint:", http.ListenAndServe(profileEndpoint, nil)) }() } // These values can also be affected by environment so set them // only if argument is specified. - if *mutexProfileFract != 0 { - runtime.SetMutexProfileFraction(*mutexProfileFract) + if c.IsSet("debug.mutexproffract") { + runtime.SetMutexProfileFraction(c.Int("debug.mutexproffract")) } - if *blockProfileRate != 0 { - runtime.SetBlockProfileRate(*blockProfileRate) + if c.IsSet("debug.blockprofrate") { + runtime.SetBlockProfileRate(c.Int("debug.blockprofrate")) } } diff --git a/tests/cover_test.go b/tests/cover_test.go index 1c9b5a7..b43d2d9 100644 --- a/tests/cover_test.go +++ b/tests/cover_test.go @@ -36,15 +36,16 @@ https://github.com/albertito/chasquid/blob/master/coverage_test.go */ import ( + "flag" "os" "testing" "github.com/foxcpp/maddy" + "github.com/urfave/cli/v2" ) func TestMain(m *testing.M) { - // -test.* flags are registered somewhere in init() in "testing" (?) - // so calling flag.Parse() in maddy.Run() catches them up. + // -test.* flags are registered somewhere in init() in "testing" (?). // maddy.Run changes the working directory, we need to change it back so // -test.coverprofile writes out profile in the right location. @@ -53,7 +54,16 @@ func TestMain(m *testing.M) { panic(err) } - code := maddy.Run() + flag.Parse() + + app := cli.NewApp() + // maddycli wrapper registers all necessary flags with flag.CommandLine by default + ctx := cli.NewContext(app, flag.CommandLine, nil) + err = maddy.Run(ctx) + code := 0 + if ec, ok := err.(cli.ExitCoder); ok { + code = ec.ExitCode() + } if err := os.Chdir(wd); err != nil { panic(err)