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)