diff --git a/cli/papero/main.go b/cli/papero/main.go index 8c54dd5..0f2a296 100644 --- a/cli/papero/main.go +++ b/cli/papero/main.go @@ -17,8 +17,10 @@ var Log *log.Logger func init() { info := InfoCmd{} + set := SetCmd{} Session.Info.Commands = cli.CommandMap{ "info": info, + "set": set, } Session.Info.Name = "papero" flag.Usage = func() { cli.Usage(os.Stdout, Session.Info) } diff --git a/cli/papero/set.go b/cli/papero/set.go new file mode 100644 index 0000000..e95e1a6 --- /dev/null +++ b/cli/papero/set.go @@ -0,0 +1,215 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "os" + "strconv" + "strings" + + "git.lattuga.net/blallo/papero/cli" + "git.lattuga.net/blallo/papero/imaputils" + "github.com/emersion/go-imap" +) + +var setVerbs cli.CommandMap + +func init() { + setVerbs = cli.CommandMap{ + "message": SetMessageCmd{}, + // "subscribe": SetSubscribedCmd{}, + // "unsubscribe": SetUnsubscribedCmd{}, + } +} + +type SetCmd struct{} + +func (s SetCmd) Func(args []string) error { + flagset := flag.NewFlagSet(args[0], flag.ExitOnError) + flagset.Usage = func() { s.Help(os.Stdout, flagset) } + flagset.Parse(args[1:]) + subArgs := flagset.Args() + if len(subArgs) == 0 { + s.Help(os.Stderr, flagset) + os.Exit(1) + } + cmd := subArgs[0] + if Session.Info.Opts.Debug { + Log.Debug("set verb:", cmd) + } + + return setVerbs[cmd].Func(subArgs) +} + +func (s SetCmd) Help(w io.Writer, set *flag.FlagSet) { + fmt.Fprintf(w, "USAGE: %s set VERB [verb opts]\n", Session.Info.Name) + fmt.Fprintf(w, "\nVERBS:\n") + for verb := range setVerbs { + fmt.Fprintf(w, "\t%s\n", verb) + } +} + +// FLAG UTILS + +type pair struct { + set, unset bool +} + +type pairFlag struct { + *pair +} + +func newPairFlag() pairFlag { + return pairFlag{ + &pair{false, false}, + } +} + +func (p pair) String() string { + switch p { + case pair{true, false}: + return "set" + case pair{false, true}: + return "unset" + default: + return "undef" + } +} + +func (pf pairFlag) String() string { + return pf.pair.String() +} + +func (pf pairFlag) Set(s string) error { + Log.Debugf("into Set: %s\n", s) + if s == "set" { + Log.Debug("setting") + v := pair{true, false} + *pf.pair = v + return nil + } + + if s == "unset" { + Log.Debug("unsetting") + v := pair{false, true} + *pf.pair = v + return nil + } + + Log.Debug(fmt.Sprintf("unacceptable value %s\n", s)) + return errors.New("only `set` or `unset` are allowed") +} + +// VERBS + +type SetMessageCmd struct{} + +func (sm SetMessageCmd) Func(args []string) error { + if Session.Info.Opts.Debug { + Log.Debug("enter set message") + } + flags := imaputils.NewFlagsStatus() + var opts imaputils.SetFlagsOpts + flagSeen := newPairFlag() + flagAnswered := newPairFlag() + flagFlagged := newPairFlag() + flagDeleted := newPairFlag() + flagDraft := newPairFlag() + + flagset := flag.NewFlagSet(args[0], flag.ExitOnError) + + flagset.Var(&flagSeen, "seen", "Set or unset `seen` flag on message") + flagset.Var(&flagAnswered, "answered", "Set or unset `answered` flag on message") + flagset.Var(&flagFlagged, "flagged", "Set or unset `flagged` flag on message") + flagset.Var(&flagDeleted, "deleted", "Set or unset `deleted` flag on message") + flagset.Var(&flagDraft, "draft", "Set or unset `draft` flag on message") + + flagset.Usage = func() { + sm.Help(os.Stdout, flagset) + } + flagset.Parse(args[1:]) + + subArgs := flagset.Args() + if len(subArgs) < 2 { + Log.Debugf("Too few arguments: %s\n", subArgs) + sm.Help(os.Stderr, flagset) + os.Exit(1) + } + + opts.Mailbox = subArgs[0] + seq := new(imap.SeqSet) + for _, msgId := range subArgs[1:] { + if err := parseMessageSeq(seq, msgId); err != nil { + return err + } + } + opts.MessageSeq = seq + opts.Debug = Session.Info.Opts.Debug + + if err := assignFlag(*flagSeen.pair, flags.SetSeen, flags.UnsetSeen); err != nil { + return err + } + if err := assignFlag(*flagAnswered.pair, flags.SetAnswered, flags.UnsetAnswered); err != nil { + return err + } + if err := assignFlag(*flagFlagged.pair, flags.SetFlagged, flags.UnsetFlagged); err != nil { + return err + } + if err := assignFlag(*flagDeleted.pair, flags.SetDeleted, flags.UnsetDeleted); err != nil { + return err + } + if err := assignFlag(*flagDraft.pair, flags.SetDraft, flags.UnsetDraft); err != nil { + return err + } + + Log.Debugf("Setting flags: %s\n", flags) + opts.Flags = flags + + return imaputils.SetFlags(Session.Config, &opts) +} + +func (sm SetMessageCmd) Help(w io.Writer, set *flag.FlagSet) { + fmt.Fprintf(w, "USAGE: %s set message [opts] MAILBOX MESSAGE_ID_OR_RANGE [MESSAGE_ID_OR_RANGE [MESSAGE_ID_OR_RANGE ...]]\n", Session.Info.Name) + fmt.Fprintln(w, "\nMESSAGE_ID_OR_RANGE being a single message id or a git-like range") + fmt.Fprintln(w, "Example:") + fmt.Fprintln(w, "\t12345\t- A single message") + fmt.Fprintln(w, "\t123...258\t- A range from id 123 to id 258") + fmt.Fprintf(w, "\nOPTS:\n") + set.PrintDefaults() +} + +func parseMessageSeq(seq *imap.SeqSet, data string) error { + if strings.Contains(data, "..") { + split := strings.Split(data, "..") + start, err := strconv.ParseUint(split[0], 10, 0) + if err != nil { + return err + } + stop, err := strconv.ParseUint(split[2], 10, 0) + if err != nil { + return err + } + seq.AddRange(uint32(start), uint32(stop)) + } else { + msg, err := strconv.ParseUint(data, 10, 0) + if err != nil { + return err + } + seq.AddNum(uint32(msg)) + } + return nil +} + +func assignFlag(flagOp pair, setCallback, unsetCallback func()) error { + switch flagOp { + case pair{true, false}: + setCallback() + case pair{false, true}: + unsetCallback() + default: + return nil + } + return nil +} diff --git a/imaputils/modify.go b/imaputils/modify.go new file mode 100644 index 0000000..9506964 --- /dev/null +++ b/imaputils/modify.go @@ -0,0 +1,233 @@ +package imaputils + +import ( + "errors" + "fmt" + + "git.lattuga.net/blallo/papero/config" + "github.com/emersion/go-imap" +) + +type tristate int + +const ( + undef tristate = 0 + doSet tristate = 1 + doUnset tristate = 2 +) + +func (t tristate) IsSet() bool { + switch t { + case undef: + return false + case doSet: + return true + case doUnset: + return false + default: + panic(errors.New(fmt.Sprintf("unexpected tristate value %d", t))) + } +} + +func (t tristate) IsUnset() bool { + switch t { + case undef: + return false + case doSet: + return false + case doUnset: + return true + default: + panic(errors.New(fmt.Sprintf("unexpected tristate value %d", t))) + } +} + +func (t tristate) String() string { + switch t { + case undef: + return "undef" + case doSet: + return "set" + case doUnset: + return "unset" + } + panic("unexpected value for tristate") +} + +type FlagsStatus struct { + FlagSeen tristate + FlagAnswered tristate + FlagFlagged tristate + FlagDeleted tristate + FlagDraft tristate + //FlagUnknown bool +} + +func NewFlagsStatus() *FlagsStatus { + return &FlagsStatus{ + undef, + undef, + undef, + undef, + undef, + } +} + +func (f *FlagsStatus) IntoSetFlagList() []interface{} { + var result []interface{} + switch { + case f.FlagSeen.IsSet(): + result = append(result, imap.SeenFlag) + case f.FlagAnswered.IsSet(): + result = append(result, imap.AnsweredFlag) + case f.FlagFlagged.IsSet(): + result = append(result, imap.FlaggedFlag) + case f.FlagDeleted.IsSet(): + result = append(result, imap.DeletedFlag) + case f.FlagDraft.IsSet(): + result = append(result, imap.DraftFlag) + } + return result +} + +func (f *FlagsStatus) IntoUnsetFlagList() []interface{} { + var result []interface{} + switch { + case f.FlagSeen.IsUnset(): + result = append(result, imap.SeenFlag) + case f.FlagAnswered.IsUnset(): + result = append(result, imap.AnsweredFlag) + case f.FlagFlagged.IsUnset(): + result = append(result, imap.FlaggedFlag) + case f.FlagDeleted.IsUnset(): + result = append(result, imap.DeletedFlag) + case f.FlagDraft.IsUnset(): + result = append(result, imap.DraftFlag) + } + return result +} + +func (f *FlagsStatus) WillSet() bool { + switch { + case f.FlagSeen.IsSet(): + return true + case f.FlagAnswered.IsSet(): + return true + case f.FlagFlagged.IsSet(): + return true + case f.FlagDeleted.IsSet(): + return true + case f.FlagDraft.IsSet(): + return true + } + return false +} + +func (f *FlagsStatus) WillUnset() bool { + switch { + case f.FlagSeen.IsUnset(): + return true + case f.FlagAnswered.IsUnset(): + return true + case f.FlagFlagged.IsUnset(): + return true + case f.FlagDeleted.IsUnset(): + return true + case f.FlagDraft.IsUnset(): + return true + } + return false +} + +func (f *FlagsStatus) SetSeen() { + f.FlagSeen = doSet +} + +func (f *FlagsStatus) UnsetSeen() { + f.FlagSeen = doUnset +} + +func (f *FlagsStatus) SetAnswered() { + f.FlagAnswered = doSet +} + +func (f *FlagsStatus) UnsetAnswered() { + f.FlagAnswered = doUnset +} + +func (f *FlagsStatus) SetFlagged() { + f.FlagFlagged = doSet +} + +func (f *FlagsStatus) UnsetFlagged() { + f.FlagFlagged = doUnset +} + +func (f *FlagsStatus) SetDeleted() { + f.FlagDeleted = doSet +} + +func (f *FlagsStatus) UnsetDeleted() { + f.FlagDeleted = doUnset +} + +func (f *FlagsStatus) SetDraft() { + f.FlagDraft = doSet +} + +func (f *FlagsStatus) UnsetDraft() { + f.FlagDraft = doUnset +} + +//func (f *FlagsStatus) SetUnknown() { +// f.FlagUnknown = doSet +//} + +//func (f *FlagsStatus) UnsetUnknown() { +// f.FlagUnknown = doUnset +//} + +type SetFlagsOpts struct { + Mailbox string + MessageSeq *imap.SeqSet + Flags *FlagsStatus + Debug bool +} + +// SetFlags changes the flags on the specified message in the specified mailbox. +// First it adds the new flags set, then in removes the flags explicitly unset. +// Unfortunately, this operation is not atomic. +func SetFlags(conf *config.AccountData, opts *SetFlagsOpts) error { + conn := NewConnection(conf) + + err := conn.Start(opts.Debug) + if err != nil { + return err + } + defer conn.Close() + + _, err = conn.client.Select(opts.Mailbox, false) + if err != nil { + return err + } + + if opts.Flags.WillSet() { + item := imap.FormatFlagsOp(imap.AddFlags, true) + flags := opts.Flags.IntoSetFlagList() + err = conn.client.Store(opts.MessageSeq, item, flags, nil) + if err != nil { + return err + } + } + + if opts.Flags.WillUnset() { + item := imap.FormatFlagsOp(imap.RemoveFlags, true) + flags := opts.Flags.IntoUnsetFlagList() + err = conn.client.Store(opts.MessageSeq, item, flags, nil) + if err != nil { + return err + } + } + + return nil +}