mirror of
https://github.com/foxcpp/maddy.git
synced 2025-04-03 05:07:38 +03:00
parent
9b1ee1b52f
commit
c0eacfa0f3
25 changed files with 1343 additions and 1384 deletions
|
@ -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"]
|
||||
|
|
9
build.sh
9
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"
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -19,11 +19,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
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()
|
||||
}
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
/*
|
||||
Maddy Mail Server - Composable all-in-one email server.
|
||||
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
|
@ -1,822 +0,0 @@
|
|||
/*
|
||||
Maddy Mail Server - Composable all-in-one email server.
|
||||
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
/*
|
||||
Maddy Mail Server - Composable all-in-one email server.
|
||||
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
2
dist/systemd/maddy.service
vendored
2
dist/systemd/maddy.service
vendored
|
@ -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
|
||||
|
|
2
dist/systemd/maddy@.service
vendored
2
dist/systemd/maddy@.service
vendored
|
@ -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
|
||||
|
|
|
@ -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 <fox.cpp@disroot.org>. 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
|
|
@ -21,9 +21,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
105
internal/cli/app.go
Normal file
105
internal/cli/app.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package main
|
||||
package ctl
|
||||
|
||||
import (
|
||||
"fmt"
|
|
@ -16,19 +16,61 @@ You should have received a copy of the GNU General Public License
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
|
@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
292
internal/cli/ctl/imapacct.go
Normal file
292
internal/cli/ctl/imapacct.go
Normal file
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
Maddy Mail Server - Composable all-in-one email server.
|
||||
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
133
internal/cli/ctl/moduleinit.go
Normal file
133
internal/cli/ctl/moduleinit.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
Maddy Mail Server - Composable all-in-one email server.
|
||||
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
246
internal/cli/ctl/users.go
Normal file
246
internal/cli/ctl/users.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
Maddy Mail Server - Composable all-in-one email server.
|
||||
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
|
@ -20,8 +20,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
142
maddy.go
142
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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue