maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package ctl
 20
 21import (
 22	"errors"
 23	"fmt"
 24	"os"
 25
 26	"github.com/emersion/go-imap"
 27	"github.com/foxcpp/maddy/framework/module"
 28	maddycli "github.com/foxcpp/maddy/internal/cli"
 29	clitools2 "github.com/foxcpp/maddy/internal/cli/clitools"
 30	"github.com/urfave/cli/v2"
 31)
 32
 33func init() {
 34	maddycli.AddSubcommand(
 35		&cli.Command{
 36			Name:  "imap-acct",
 37			Usage: "IMAP storage accounts management",
 38			Description: `These subcommands can be used to list/create/delete IMAP storage
 39accounts for any storage backend supported by maddy.
 40
 41The corresponding storage backend should be configured in maddy.conf and be
 42defined in a top-level configuration block. By default, the name of that
 43block should be local_mailboxes but this can be changed using --cfg-block
 44flag for subcommands.
 45
 46Note that in default configuration it is not enough to create an IMAP storage
 47account to grant server access. Additionally, user credentials should
 48be created using 'creds' subcommand.
 49`,
 50			Subcommands: []*cli.Command{
 51				{
 52					Name:  "list",
 53					Usage: "List storage accounts",
 54					Flags: []cli.Flag{
 55						&cli.StringFlag{
 56							Name:    "cfg-block",
 57							Usage:   "Module configuration block to use",
 58							EnvVars: []string{"MADDY_CFGBLOCK"},
 59							Value:   "local_mailboxes",
 60						},
 61					},
 62					Action: func(ctx *cli.Context) error {
 63						be, err := openStorage(ctx)
 64						if err != nil {
 65							return err
 66						}
 67						defer closeIfNeeded(be)
 68						return imapAcctList(be, ctx)
 69					},
 70				},
 71				{
 72					Name:  "create",
 73					Usage: "Create IMAP storage account",
 74					Description: `In addition to account creation, this command
 75creates a set of default folder (mailboxes) with special-use attribute set.`,
 76					ArgsUsage: "USERNAME",
 77					Flags: []cli.Flag{
 78						&cli.StringFlag{
 79							Name:    "cfg-block",
 80							Usage:   "Module configuration block to use",
 81							EnvVars: []string{"MADDY_CFGBLOCK"},
 82							Value:   "local_mailboxes",
 83						},
 84						&cli.BoolFlag{
 85							Name:  "no-specialuse",
 86							Usage: "Do not create special-use folders",
 87							Value: false,
 88						},
 89						&cli.StringFlag{
 90							Name:  "sent-name",
 91							Usage: "Name of special mailbox for sent messages, use empty string to not create any",
 92							Value: "Sent",
 93						},
 94						&cli.StringFlag{
 95							Name:  "trash-name",
 96							Usage: "Name of special mailbox for trash, use empty string to not create any",
 97							Value: "Trash",
 98						},
 99						&cli.StringFlag{
100							Name:  "junk-name",
101							Usage: "Name of special mailbox for 'junk' (spam), use empty string to not create any",
102							Value: "Junk",
103						},
104						&cli.StringFlag{
105							Name:  "drafts-name",
106							Usage: "Name of special mailbox for drafts, use empty string to not create any",
107							Value: "Drafts",
108						},
109						&cli.StringFlag{
110							Name:  "archive-name",
111							Usage: "Name of special mailbox for archive, use empty string to not create any",
112							Value: "Archive",
113						},
114					},
115					Action: func(ctx *cli.Context) error {
116						be, err := openStorage(ctx)
117						if err != nil {
118							return err
119						}
120						defer closeIfNeeded(be)
121						return imapAcctCreate(be, ctx)
122					},
123				},
124				{
125					Name:  "remove",
126					Usage: "Delete IMAP storage account",
127					Description: `If IMAP connections are open and using the specified account,
128messages access will be killed off immediately though connection will remain open. No cache
129or other buffering takes effect.`,
130					ArgsUsage: "USERNAME",
131					Flags: []cli.Flag{
132						&cli.StringFlag{
133							Name:    "cfg-block",
134							Usage:   "Module configuration block to use",
135							EnvVars: []string{"MADDY_CFGBLOCK"},
136							Value:   "local_mailboxes",
137						},
138						&cli.BoolFlag{
139							Name:    "yes",
140							Aliases: []string{"y"},
141							Usage:   "Don't ask for confirmation",
142						},
143					},
144					Action: func(ctx *cli.Context) error {
145						be, err := openStorage(ctx)
146						if err != nil {
147							return err
148						}
149						defer closeIfNeeded(be)
150						return imapAcctRemove(be, ctx)
151					},
152				},
153				{
154					Name:  "appendlimit",
155					Usage: "Query or set accounts's APPENDLIMIT value",
156					Description: `APPENDLIMIT value determines the size of a message that
157can be saved into a mailbox using IMAP APPEND command. This does not affect the size
158of messages that can be delivered to the mailbox from non-IMAP sources (e.g. SMTP).
159
160Global APPENDLIMIT value set via server configuration takes precedence over
161per-account values configured using this command.
162
163APPENDLIMIT value (either global or per-account) cannot be larger than
1644 GiB due to IMAP protocol limitations.
165`,
166					ArgsUsage: "USERNAME",
167					Flags: []cli.Flag{
168						&cli.StringFlag{
169							Name:    "cfg-block",
170							Usage:   "Module configuration block to use",
171							EnvVars: []string{"MADDY_CFGBLOCK"},
172							Value:   "local_mailboxes",
173						},
174						&cli.IntFlag{
175							Name:    "value",
176							Aliases: []string{"v"},
177							Usage:   "Set APPENDLIMIT to specified value (in bytes)",
178						},
179					},
180					Action: func(ctx *cli.Context) error {
181						be, err := openStorage(ctx)
182						if err != nil {
183							return err
184						}
185						defer closeIfNeeded(be)
186						return imapAcctAppendlimit(be, ctx)
187					},
188				},
189			},
190		})
191}
192
193type SpecialUseUser interface {
194	CreateMailboxSpecial(name, specialUseAttr string) error
195}
196
197func imapAcctList(be module.Storage, ctx *cli.Context) error {
198	mbe, ok := be.(module.ManageableStorage)
199	if !ok {
200		return cli.Exit("Error: storage backend does not support accounts management using maddy command", 2)
201	}
202
203	list, err := mbe.ListIMAPAccts()
204	if err != nil {
205		return err
206	}
207
208	if len(list) == 0 && !ctx.Bool("quiet") {
209		fmt.Fprintln(os.Stderr, "No users.")
210	}
211
212	for _, user := range list {
213		fmt.Println(user)
214	}
215	return nil
216}
217
218func imapAcctCreate(be module.Storage, ctx *cli.Context) error {
219	mbe, ok := be.(module.ManageableStorage)
220	if !ok {
221		return cli.Exit("Error: storage backend does not support accounts management using maddy command", 2)
222	}
223
224	username := ctx.Args().First()
225	if username == "" {
226		return cli.Exit("Error: USERNAME is required", 2)
227	}
228
229	if err := mbe.CreateIMAPAcct(username); err != nil {
230		return err
231	}
232
233	act, err := mbe.GetIMAPAcct(username)
234	if err != nil {
235		return fmt.Errorf("failed to get user: %w", err)
236	}
237
238	suu, ok := act.(SpecialUseUser)
239	if !ok {
240		fmt.Fprintf(os.Stderr, "Note: Storage backend does not support SPECIAL-USE IMAP extension")
241	}
242
243	if ctx.Bool("no-specialuse") {
244		return nil
245	}
246
247	createMbox := func(name, specialUseAttr string) error {
248		if suu == nil {
249			return act.CreateMailbox(name)
250		}
251		return suu.CreateMailboxSpecial(name, specialUseAttr)
252	}
253
254	if name := ctx.String("sent-name"); name != "" {
255		if err := createMbox(name, imap.SentAttr); err != nil {
256			fmt.Fprintf(os.Stderr, "Failed to create sent folder: %v", err)
257		}
258	}
259	if name := ctx.String("trash-name"); name != "" {
260		if err := createMbox(name, imap.TrashAttr); err != nil {
261			fmt.Fprintf(os.Stderr, "Failed to create trash folder: %v", err)
262		}
263	}
264	if name := ctx.String("junk-name"); name != "" {
265		if err := createMbox(name, imap.JunkAttr); err != nil {
266			fmt.Fprintf(os.Stderr, "Failed to create junk folder: %v", err)
267		}
268	}
269	if name := ctx.String("drafts-name"); name != "" {
270		if err := createMbox(name, imap.DraftsAttr); err != nil {
271			fmt.Fprintf(os.Stderr, "Failed to create drafts folder: %v", err)
272		}
273	}
274	if name := ctx.String("archive-name"); name != "" {
275		if err := createMbox(name, imap.ArchiveAttr); err != nil {
276			fmt.Fprintf(os.Stderr, "Failed to create archive folder: %v", err)
277		}
278	}
279
280	return nil
281}
282
283func imapAcctRemove(be module.Storage, ctx *cli.Context) error {
284	mbe, ok := be.(module.ManageableStorage)
285	if !ok {
286		return cli.Exit("Error: storage backend does not support accounts management using maddy command", 2)
287	}
288
289	username := ctx.Args().First()
290	if username == "" {
291		return cli.Exit("Error: USERNAME is required", 2)
292	}
293
294	if !ctx.Bool("yes") {
295		if !clitools2.Confirmation("Are you sure you want to delete this user account?", false) {
296			return errors.New("Cancelled")
297		}
298	}
299
300	return mbe.DeleteIMAPAcct(username)
301}