mirror of
https://github.com/foxcpp/maddy.git
synced 2025-04-05 14:07:38 +03:00
There is abstraction 'updates pipe' defined for future use with configuration involving IMAP data replication (e.g. multiple nodes with maddy instances + PostgreSQL replicas + S3 bucket for messages). However, for the case of local SQLite3 DB, limited UDS-based implementation is provided. It solves the problem of maddyctl not being able to tell the server about modifications it makes. Alternative to this approach would be to have server actually perform operations and maddyctl being a dumb API client, but this requires a lot more complex IPC interface and will not work when the server is down.
494 lines
10 KiB
Go
494 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/emersion/go-imap"
|
|
imapsql "github.com/foxcpp/go-imap-sql"
|
|
"github.com/foxcpp/maddy/cmd/maddyctl/clitools"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
func FormatAddress(addr *imap.Address) string {
|
|
return fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName)
|
|
}
|
|
|
|
func FormatAddressList(addrs []*imap.Address) string {
|
|
res := make([]string, 0, len(addrs))
|
|
for _, addr := range addrs {
|
|
res = append(res, FormatAddress(addr))
|
|
}
|
|
return strings.Join(res, ", ")
|
|
}
|
|
|
|
func mboxesList(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mboxes, err := u.ListMailboxes(ctx.Bool("subscribed,s"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(mboxes) == 0 && !ctx.GlobalBool("quiet") {
|
|
fmt.Fprintln(os.Stderr, "No mailboxes.")
|
|
}
|
|
|
|
for _, mbox := range mboxes {
|
|
info, err := mbox.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(info.Attributes) != 0 {
|
|
fmt.Print(info.Name, "\t", info.Attributes, "\n")
|
|
} else {
|
|
fmt.Println(info.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func mboxesCreate(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
name := ctx.Args().Get(1)
|
|
if name == "" {
|
|
return errors.New("Error: NAME is required")
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ctx.IsSet("special") {
|
|
attr := "\\" + strings.Title(ctx.String("special"))
|
|
return u.(*imapsql.User).CreateMailboxSpecial(name, attr)
|
|
}
|
|
|
|
return u.CreateMailbox(name)
|
|
}
|
|
|
|
func mboxesRemove(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
name := ctx.Args().Get(1)
|
|
if name == "" {
|
|
return errors.New("Error: NAME is required")
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mbox, err := u.GetMailbox(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !ctx.Bool("yes,y") {
|
|
status, err := mbox.Status([]imap.StatusItem{imap.StatusMessages})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if status.Messages != 0 {
|
|
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) {
|
|
return errors.New("Cancelled")
|
|
}
|
|
}
|
|
|
|
if err := u.DeleteMailbox(name); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func mboxesRename(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
oldName := ctx.Args().Get(1)
|
|
if oldName == "" {
|
|
return errors.New("Error: OLDNAME is required")
|
|
}
|
|
newName := ctx.Args().Get(2)
|
|
if newName == "" {
|
|
return errors.New("Error: NEWNAME is required")
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return u.RenameMailbox(oldName, newName)
|
|
}
|
|
|
|
func msgsAdd(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
name := ctx.Args().Get(1)
|
|
if name == "" {
|
|
return errors.New("Error: MAILBOX is required")
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mbox, err := u.GetMailbox(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
flags := ctx.StringSlice("flag")
|
|
if flags == nil {
|
|
flags = []string{}
|
|
}
|
|
|
|
date := time.Now()
|
|
if ctx.IsSet("date") {
|
|
date = time.Unix(ctx.Int64("date"), 0)
|
|
}
|
|
|
|
buf := bytes.Buffer{}
|
|
if _, err := io.Copy(&buf, os.Stdin); err != nil {
|
|
return err
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
return errors.New("Error: Empty message, refusing to continue")
|
|
}
|
|
|
|
status, err := mbox.Status([]imap.StatusItem{imap.StatusUidNext})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := mbox.CreateMessage(flags, date, &buf); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(status.UidNext)
|
|
|
|
return nil
|
|
}
|
|
|
|
func msgsRemove(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
name := ctx.Args().Get(1)
|
|
if name == "" {
|
|
return errors.New("Error: MAILBOX is required")
|
|
}
|
|
seqset := ctx.Args().Get(2)
|
|
if seqset == "" {
|
|
return errors.New("Error: SEQSET is required")
|
|
}
|
|
|
|
seq, err := imap.ParseSeqSet(seqset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mbox, err := u.GetMailbox(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !ctx.Bool("yes") {
|
|
if !clitools.Confirmation("Are you sure you want to delete these messages?", false) {
|
|
return errors.New("Cancelled")
|
|
}
|
|
}
|
|
|
|
mboxB := mbox.(*imapsql.Mailbox)
|
|
if err := mboxB.DelMessages(ctx.Bool("uid"), seq); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func msgsCopy(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
srcName := ctx.Args().Get(1)
|
|
if srcName == "" {
|
|
return errors.New("Error: SRCMAILBOX is required")
|
|
}
|
|
seqset := ctx.Args().Get(2)
|
|
if seqset == "" {
|
|
return errors.New("Error: SEQSET is required")
|
|
}
|
|
tgtName := ctx.Args().Get(3)
|
|
if tgtName == "" {
|
|
return errors.New("Error: TGTMAILBOX is required")
|
|
}
|
|
|
|
seq, err := imap.ParseSeqSet(seqset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
srcMbox, err := u.GetMailbox(srcName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return srcMbox.CopyMessages(ctx.Bool("uid"), seq, tgtName)
|
|
}
|
|
|
|
func msgsMove(be Storage, ctx *cli.Context) error {
|
|
if ctx.Bool("y,yes") || !clitools.Confirmation("Currently, it is unsafe to remove messages from mailboxes used by connected clients, continue?", false) {
|
|
return errors.New("Cancelled")
|
|
}
|
|
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
srcName := ctx.Args().Get(1)
|
|
if srcName == "" {
|
|
return errors.New("Error: SRCMAILBOX is required")
|
|
}
|
|
seqset := ctx.Args().Get(2)
|
|
if seqset == "" {
|
|
return errors.New("Error: SEQSET is required")
|
|
}
|
|
tgtName := ctx.Args().Get(3)
|
|
if tgtName == "" {
|
|
return errors.New("Error: TGTMAILBOX is required")
|
|
}
|
|
|
|
seq, err := imap.ParseSeqSet(seqset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
srcMbox, err := u.GetMailbox(srcName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
moveMbox := srcMbox.(*imapsql.Mailbox)
|
|
|
|
return moveMbox.MoveMessages(ctx.Bool("uid"), seq, tgtName)
|
|
}
|
|
|
|
func msgsList(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
mboxName := ctx.Args().Get(1)
|
|
if mboxName == "" {
|
|
return errors.New("Error: MAILBOX is required")
|
|
}
|
|
seqset := ctx.Args().Get(2)
|
|
if seqset == "" {
|
|
seqset = "*"
|
|
}
|
|
|
|
seq, err := imap.ParseSeqSet(seqset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mbox, err := u.GetMailbox(mboxName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ch := make(chan *imap.Message, 10)
|
|
go func() {
|
|
err = mbox.ListMessages(ctx.Bool("uid"), seq, []imap.FetchItem{imap.FetchEnvelope, imap.FetchInternalDate, imap.FetchRFC822Size, imap.FetchFlags, imap.FetchUid}, ch)
|
|
}()
|
|
|
|
for msg := range ch {
|
|
if !ctx.Bool("full") {
|
|
fmt.Printf("UID %d: %s - %s\n %v, %v\n\n", msg.Uid, FormatAddressList(msg.Envelope.From), msg.Envelope.Subject, msg.Flags, msg.Envelope.Date)
|
|
continue
|
|
}
|
|
|
|
fmt.Println("- Server meta-data:")
|
|
fmt.Println("UID:", msg.Uid)
|
|
fmt.Println("Sequence number:", msg.SeqNum)
|
|
fmt.Println("Flags:", msg.Flags)
|
|
fmt.Println("Body size:", msg.Size)
|
|
fmt.Println("Internal date:", msg.InternalDate.Unix(), msg.InternalDate)
|
|
fmt.Println("- Envelope:")
|
|
if len(msg.Envelope.From) != 0 {
|
|
fmt.Println("From:", FormatAddressList(msg.Envelope.From))
|
|
}
|
|
if len(msg.Envelope.To) != 0 {
|
|
fmt.Println("To:", FormatAddressList(msg.Envelope.To))
|
|
}
|
|
if len(msg.Envelope.Cc) != 0 {
|
|
fmt.Println("CC:", FormatAddressList(msg.Envelope.Cc))
|
|
}
|
|
if len(msg.Envelope.Bcc) != 0 {
|
|
fmt.Println("BCC:", FormatAddressList(msg.Envelope.Bcc))
|
|
}
|
|
if msg.Envelope.InReplyTo != "" {
|
|
fmt.Println("In-Reply-To:", msg.Envelope.InReplyTo)
|
|
}
|
|
if msg.Envelope.MessageId != "" {
|
|
fmt.Println("Message-Id:", msg.Envelope.MessageId)
|
|
}
|
|
if !msg.Envelope.Date.IsZero() {
|
|
fmt.Println("Date:", msg.Envelope.Date.Unix(), msg.Envelope.Date)
|
|
}
|
|
if msg.Envelope.Subject != "" {
|
|
fmt.Println("Subject:", msg.Envelope.Subject)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func msgsDump(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
mboxName := ctx.Args().Get(1)
|
|
if mboxName == "" {
|
|
return errors.New("Error: MAILBOX is required")
|
|
}
|
|
seqset := ctx.Args().Get(2)
|
|
if seqset == "" {
|
|
seqset = "*"
|
|
}
|
|
|
|
seq, err := imap.ParseSeqSet(seqset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mbox, err := u.GetMailbox(mboxName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ch := make(chan *imap.Message, 10)
|
|
go func() {
|
|
err = mbox.ListMessages(ctx.Bool("uid"), seq, []imap.FetchItem{imap.FetchRFC822}, ch)
|
|
}()
|
|
|
|
for msg := range ch {
|
|
for _, v := range msg.Body {
|
|
if _, err := io.Copy(os.Stdout, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func msgsFlags(be Storage, ctx *cli.Context) error {
|
|
username := ctx.Args().First()
|
|
if username == "" {
|
|
return errors.New("Error: USERNAME is required")
|
|
}
|
|
name := ctx.Args().Get(1)
|
|
if name == "" {
|
|
return errors.New("Error: MAILBOX is required")
|
|
}
|
|
seqStr := ctx.Args().Get(2)
|
|
if seqStr == "" {
|
|
return errors.New("Error: SEQ is required")
|
|
}
|
|
|
|
seq, err := imap.ParseSeqSet(seqStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
u, err := be.GetUser(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mbox, err := u.GetMailbox(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
flags := ctx.Args()[3:]
|
|
if len(flags) == 0 {
|
|
return errors.New("Error: at least once FLAG is required")
|
|
}
|
|
|
|
var op imap.FlagsOp
|
|
switch ctx.Command.Name {
|
|
case "add-flags":
|
|
op = imap.AddFlags
|
|
case "rem-flags":
|
|
op = imap.RemoveFlags
|
|
case "set-flags":
|
|
op = imap.SetFlags
|
|
default:
|
|
panic("unknown command: " + ctx.Command.Name)
|
|
}
|
|
|
|
return mbox.UpdateMessagesFlags(ctx.IsSet("uid"), seq, op, flags)
|
|
}
|