maddy/cmd/maddyctl/imap.go
fox.cpp a574b9fbb2
Use Unix socket to pass IMAP updates from maddyctl to daemon
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.
2019-12-13 17:31:35 +03:00

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)
}