mirror of
https://github.com/foxcpp/maddy.git
synced 2025-04-05 05:57:39 +03:00
Now it is not tied go-imap-sql details (with the exception of special features), allowing it to be used with other storage backends that will be added in the future. --unsafe flag is removed and now maddyctl explicitly asks for confirmation in cases where transaction may be unsafe for connected clients. --yes flag disables that. In the future, maddy can be extended with IPC interface to push updates so it this restriction can be lifted altogether.
635 lines
16 KiB
Go
635 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/emersion/go-imap/backend"
|
|
"github.com/urfave/cli"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type UserDB interface {
|
|
ListUsers() ([]string, error)
|
|
CreateUser(username, password string) error
|
|
CreateUserNoPass(username string) error
|
|
DeleteUser(username string) error
|
|
SetUserPassword(username, newPassword string) error
|
|
Close() error
|
|
}
|
|
|
|
type Storage interface {
|
|
GetUser(username string) (backend.User, error)
|
|
Close() error
|
|
}
|
|
|
|
func main() {
|
|
app := cli.NewApp()
|
|
app.Name = "maddyctl"
|
|
app.Usage = "maddy mail server administration utility"
|
|
app.Version = buildInfo()
|
|
|
|
app.Flags = []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "config",
|
|
Usage: "Configuration file to use",
|
|
EnvVar: "MADDY_CONFIG",
|
|
Value: "/etc/maddy/maddy.conf",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "state",
|
|
Usage: "State directory to use",
|
|
EnvVar: "MADDY_STATE",
|
|
Value: "/var/lib/maddy",
|
|
},
|
|
}
|
|
|
|
app.Commands = []cli.Command{
|
|
{
|
|
Name: "users",
|
|
Usage: "User accounts management",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "list",
|
|
Usage: "List created user accounts",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "cfg-block",
|
|
Usage: "Module configuration block to use",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_authdb",
|
|
},
|
|
},
|
|
Action: func(ctx *cli.Context) error {
|
|
be, err := openUserDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_authdb",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "password,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,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 be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_authdb",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "yes,y",
|
|
Usage: "Don't ask for confirmation",
|
|
},
|
|
},
|
|
Action: func(ctx *cli.Context) error {
|
|
be, err := openUserDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_authdb",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "password,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,n",
|
|
Usage: "Set password to null",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "hash",
|
|
Usage: "Use specified hash algorithm for password. Supported values vary depending on storage backend.",
|
|
Value: "",
|
|
},
|
|
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 be.Close()
|
|
return usersPassword(be, ctx)
|
|
},
|
|
},
|
|
{
|
|
Name: "imap-appendlimit",
|
|
Usage: "Query or set user's APPENDLIMIT value",
|
|
ArgsUsage: "USERNAME",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "cfg-block",
|
|
Usage: "Module configuration block to use",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "value,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 be.Close()
|
|
return usersAppendlimit(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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "subscribed,s",
|
|
Usage: "List only subscribed mailboxes",
|
|
},
|
|
},
|
|
Action: func(ctx *cli.Context) error {
|
|
be, err := openStorage(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer be.Close()
|
|
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",
|
|
EnvVar: "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 be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "yes,y",
|
|
Usage: "Don't ask for confirmation",
|
|
},
|
|
},
|
|
Action: func(ctx *cli.Context) error {
|
|
be, err := openStorage(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
},
|
|
Action: func(ctx *cli.Context) error {
|
|
be, err := openStorage(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "flag,f",
|
|
Usage: "Add flag to message. Can be specified multiple times",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "date,d",
|
|
Usage: "Set internal date value to specified UNIX timestamp",
|
|
},
|
|
},
|
|
Action: func(ctx *cli.Context) error {
|
|
be, err := openStorage(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "uid,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 be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "uid,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 be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "uid,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 be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "uid,u",
|
|
Usage: "Use UIDs for SEQSET instead of sequence numbers",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "yes,y",
|
|
Usage: "Don't ask for confirmation",
|
|
},
|
|
},
|
|
Action: func(ctx *cli.Context) error {
|
|
be, err := openStorage(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "uid,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 be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "uid,u",
|
|
Usage: "Use UIDs for SEQSET instead of sequence numbers",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "yes,y",
|
|
Usage: "Don't ask for confirmation",
|
|
},
|
|
},
|
|
Action: func(ctx *cli.Context) error {
|
|
be, err := openStorage(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "uid,u",
|
|
Usage: "Use UIDs for SEQSET instead of sequence numbers",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "full,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 be.Close()
|
|
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",
|
|
EnvVar: "MADDY_CFGBLOCK",
|
|
Value: "local_mailboxes",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "uid,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 be.Close()
|
|
return msgsList(be, ctx)
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := app.Run(os.Args); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
}
|
|
}
|
|
|
|
func openStorage(ctx *cli.Context) (Storage, error) {
|
|
cfgPath := ctx.GlobalString("config")
|
|
if cfgPath == "" {
|
|
return nil, errors.New("Error: config is required")
|
|
}
|
|
|
|
statePath := ctx.GlobalString("state")
|
|
if cfgPath == "" {
|
|
return nil, errors.New("Error: state is required")
|
|
}
|
|
|
|
cfgBlock := ctx.String("cfg-block")
|
|
if cfgBlock == "" {
|
|
return nil, errors.New("Error: cfg-block is required")
|
|
}
|
|
|
|
root, node, err := findBlockInCfg(cfgPath, cfgBlock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := os.Chdir(statePath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch node.Name {
|
|
case "sql":
|
|
return sqlFromCfgBlock(root, node)
|
|
default:
|
|
return nil, errors.New("Error: Storage backend is not supported by maddyctl")
|
|
}
|
|
}
|
|
|
|
func openUserDB(ctx *cli.Context) (UserDB, error) {
|
|
cfgPath := ctx.GlobalString("config")
|
|
if cfgPath == "" {
|
|
return nil, errors.New("Error: config is required")
|
|
}
|
|
|
|
statePath := ctx.GlobalString("state")
|
|
if cfgPath == "" {
|
|
return nil, errors.New("Error: state is required")
|
|
}
|
|
|
|
cfgBlock := ctx.String("cfg-block")
|
|
if cfgBlock == "" {
|
|
return nil, errors.New("Error: cfg-block is required")
|
|
}
|
|
|
|
root, node, err := findBlockInCfg(cfgPath, cfgBlock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := os.Chdir(statePath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch node.Name {
|
|
case "sql":
|
|
return sqlFromCfgBlock(root, node)
|
|
default:
|
|
return nil, errors.New("Error: Authentication backend is not supported by maddyctl")
|
|
}
|
|
}
|