From 38ee83e45b4de7edf89bf9f0ef629eb4c6ad0fa8 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Thu, 12 May 2016 23:56:07 -0400 Subject: Moving to glide --- .../mattermost/rsc/google/gmail/gmail.go | 1241 ++++++++++++++++++++ 1 file changed, 1241 insertions(+) create mode 100644 vendor/github.com/mattermost/rsc/google/gmail/gmail.go (limited to 'vendor/github.com/mattermost/rsc/google/gmail') diff --git a/vendor/github.com/mattermost/rsc/google/gmail/gmail.go b/vendor/github.com/mattermost/rsc/google/gmail/gmail.go new file mode 100644 index 000000000..1d4072ef7 --- /dev/null +++ b/vendor/github.com/mattermost/rsc/google/gmail/gmail.go @@ -0,0 +1,1241 @@ +package main + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "io" + "log" + "os" + "os/exec" + "os/signal" + "regexp" + "sort" + "strings" + "time" + + "github.com/mattermost/rsc/google" + "github.com/mattermost/rsc/imap" +) + +var cmdtab = []struct { + Name string + Args int + F func(*Cmd, *imap.MsgPart) *imap.MsgPart + TF func(*Cmd, []*imap.Msg) *imap.MsgPart + Help string +}{ + {"+", 0, pluscmd, tpluscmd, "+ print the next message"}, + {"a", 1, rcmd, nil, "a reply to sender and recipients"}, + {"b", 0, bcmd, nil, "b print the next 10 headers"}, + {"d", 0, dcmd, tdcmd, "d mark for deletion"}, + {"f", 1, fcmd, tfcmd, "f forward message"}, + {"h", 0, hcmd, nil, "h print elided message summary (,h for all)"}, + {"help", 0, nil, nil, "help print this info"}, + {"i", 0, icmd, nil, "i incorporate new mail"}, + {"m", 0, mcmd, tmcmd, "m mute and delete thread (gmail only)"}, + {"mime", 0, mimecmd, nil, "mime print message's MIME structure "}, + {"p", 0, pcmd, nil, "p print the processed message"}, + // { "p+", 0, pcmd, nil, "p print the processed message, showing all quoted text" }, + {"P", 0, Pcmd, nil, "P print the raw message"}, + {`"`, 0, quotecmd, nil, `" print a quoted version of msg`}, + {"q", 0, qcmd, nil, "q exit and remove all deleted mail"}, + {"r", 1, rcmd, nil, "r [addr] reply to sender plus any addrs specified"}, + {"s", 1, scmd, tscmd, "s name copy message to named mailbox (label for gmail)"}, + {"u", 0, ucmd, nil, "u remove deletion mark"}, + // { "w", 1, wcmd, nil, "w file store message contents as file" }, + {"W", 0, Wcmd, nil, "W open in web browser"}, + {"x", 0, xcmd, nil, "x exit without flushing deleted messages"}, + {"y", 0, ycmd, nil, "y synchronize with mail box"}, + {"=", 1, eqcmd, nil, "= print current message number"}, + {"|", 1, pipecmd, nil, "|cmd pipe message body to a command"}, + // { "||", 1, rpipecmd, nil, "||cmd pipe raw message to a command" }, + {"!", 1, bangcmd, nil, "!cmd run a command"}, +} + +func init() { + // Have to insert helpcmd by hand because it refers to cmdtab, + // so it would cause an init loop above. + for i := range cmdtab { + if cmdtab[i].Name == "help" { + cmdtab[i].F = helpcmd + } + } +} + +type Cmd struct { + Name string + Args []string + Line string // Args[0:] original text + ArgLine string // Args[1:] original text + F func(*Cmd, *imap.MsgPart) *imap.MsgPart + TF func(*Cmd, []*imap.Msg) *imap.MsgPart + Delete bool + Thread bool + Targ *imap.MsgPart + Targs []*imap.Msg + A1, A2 int +} + +var ( + bin = bufio.NewReader(os.Stdin) + bout = bufio.NewWriter(os.Stdout) + + acctName = flag.String("a", "", "account to use") + + dot *imap.MsgPart // Selected messages + + inbox *imap.Box + msgs []*imap.Msg + msgNum = make(map[*imap.Msg]int) + deleted = make(map[*imap.Msg]bool) + isGmail = false + acct google.Account + threaded bool + interrupted bool + + maxfrom int + subjlen int +) + +func nextMsg(m *imap.Msg) *imap.Msg { + i := msgNum[m] + i++ + if i >= len(msgs) { + return nil + } + return msgs[i] +} + +func main() { + flag.BoolVar(&imap.Debug, "imapdebug", false, "imap debugging trace") + flag.Parse() + + acct = google.Acct(*acctName) + + if args := flag.Args(); len(args) > 0 { + for i := range args { + args[i] = "-to=" + args[i] + } + cmd := exec.Command("gmailsend", append([]string{"-a", acct.Email, "-i"}, args...)...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "!%s\n", err) + os.Exit(1) + } + return + } + + c, err := imap.NewClient(imap.TLS, "imap.gmail.com", acct.Email, acct.Password, "") + if err != nil { + log.Fatal(err) + } + isGmail = c.IsGmail() + threaded = isGmail + + inbox = c.Inbox() + if err := inbox.Check(); err != nil { + log.Fatal(err) + } + + msgs = inbox.Msgs() + maxfrom = 12 + for i, m := range msgs { + msgNum[m] = i + if n := len(from(m.Hdr)); n > maxfrom { + maxfrom = n + } + } + if maxfrom > 20 { + maxfrom = 20 + } + subjlen = 80 - maxfrom + + rethread() + + go func() { + for sig := range signal.Incoming { + if sig == os.SIGINT { + fmt.Fprintf(os.Stderr, "!interrupt\n") + interrupted = true + continue + } + if sig == os.SIGCHLD || sig == os.SIGWINCH { + continue + } + fmt.Fprintf(os.Stderr, "!%s\n", sig) + } + }() + + for { + if dot != nil { + fmt.Fprintf(bout, "%d", msgNum[dot.Msg]+1) + if dot != &dot.Msg.Root { + fmt.Fprintf(bout, ".%s", dot.ID) + } + } + fmt.Fprintf(bout, ": ") + bout.Flush() + + line, err := bin.ReadString('\n') + if err != nil { + break + } + + cmd, err := parsecmd(line) + if err != nil { + fmt.Fprintf(bout, "!%s\n", err) + continue + } + + if cmd.Targ != nil || cmd.Targs == nil && cmd.A2 == 0 { + x := cmd.F(cmd, cmd.Targ) + if x != nil { + dot = x + } + } else { + targs := cmd.Targs + if targs == nil { + delta := +1 + if cmd.A1 > cmd.A2 { + delta = -1 + } + for i := cmd.A1; i <= cmd.A2; i += delta { + if i < 1 || i > len(msgs) { + continue + } + targs = append(targs, msgs[i-1]) + } + } + if cmd.Thread { + if !isGmail { + fmt.Fprintf(bout, "!need gmail for threaded command\n") + continue + } + byThread := make(map[uint64][]*imap.Msg) + for _, m := range msgs { + t := m.GmailThread + byThread[t] = append(byThread[t], m) + } + for _, m := range targs { + t := m.GmailThread + if byThread[t] != nil { + if cmd.TF != nil { + if x := cmd.TF(cmd, byThread[t]); x != nil { + dot = x + } + } else { + for _, mm := range byThread[t] { + x := cmd.F(cmd, &mm.Root) + if x != nil { + dot = x + } + } + } + } + delete(byThread, t) + } + continue + } + for _, m := range targs { + if cmd.Delete { + dcmd(cmd, &m.Root) + if cmd.Name == "p" { + // dp is a special case: it advances to the next message before the p. + next := nextMsg(m) + if next == nil { + fmt.Fprintf(bout, "!address\n") + dot = &m.Root + break + } + m = next + } + } + x := cmd.F(cmd, &m.Root) + if x != nil { + dot = x + } + // TODO: Break loop on interrupt. + } + } + } + qcmd(nil, nil) +} + +func parsecmd(line string) (cmd *Cmd, err error) { + cmd = &Cmd{} + line = strings.TrimSpace(line) + if line == "" { + // Empty command is a special case: advance and print. + cmd.F = pcmd + if dot == nil { + cmd.A1 = 1 + cmd.A2 = 1 + } else { + n := msgNum[dot.Msg] + 2 + if n > len(msgs) { + return nil, fmt.Errorf("out of messages") + } + cmd.A1 = n + cmd.A2 = n + } + return cmd, nil + } + + // Global search? + if line[0] == 'g' { + line = line[1:] + if line == "" || line[0] != '/' { + // No search string means all messages. + cmd.A1 = 1 + cmd.A2 = len(msgs) + } else if line[0] == '/' { + re, rest, err := parsere(line) + if err != nil { + return nil, err + } + line = rest + // Find all messages matching this search string. + var targ []*imap.Msg + for _, m := range msgs { + if re.MatchString(header(m)) { + targ = append(targ, m) + } + } + if len(targ) == 0 { + return nil, fmt.Errorf("no matches") + } + cmd.Targs = targ + } + } else { + // Parse an address. + a1, targ, rest, err := parseaddr(line, 1) + if err != nil { + return nil, err + } + if targ != nil { + cmd.Targ = targ + line = rest + } else { + if a1 < 1 || a1 > len(msgs) { + return nil, fmt.Errorf("message number %d out of range", a1) + } + cmd.A1 = a1 + cmd.A2 = a1 + a2 := a1 + if rest != "" && rest[0] == ',' { + // This is an address range. + a2, targ, rest, err = parseaddr(rest[1:], len(msgs)) + if err != nil { + return nil, err + } + if a2 < 1 || a2 > len(msgs) { + return nil, fmt.Errorf("message number %d out of range", a2) + } + cmd.A2 = a2 + } else if rest == line { + // There was no address. + if dot == nil { + cmd.A1 = 1 + cmd.A2 = 0 + } else { + if dot != nil { + if dot == &dot.Msg.Root { + // If dot is a plain msg, use a range so that dp works. + cmd.A1 = msgNum[dot.Msg] + 1 + cmd.A2 = cmd.A1 + } else { + cmd.Targ = dot + } + } + } + } + line = rest + } + } + + cmd.Line = strings.TrimSpace(line) + + // Insert space after ! or | for tokenization. + switch { + case strings.HasPrefix(cmd.Line, "||"): + cmd.Line = cmd.Line[:2] + " " + cmd.Line[2:] + case strings.HasPrefix(cmd.Line, "!"), strings.HasPrefix(cmd.Line, "|"): + cmd.Line = cmd.Line[:1] + " " + cmd.Line[1:] + } + + av := strings.Fields(cmd.Line) + cmd.Args = av + if len(av) == 0 || av[0] == "" { + // Default is to print. + cmd.F = pcmd + return cmd, nil + } + + name := av[0] + cmd.ArgLine = strings.TrimSpace(cmd.Line[len(av[0]):]) + + // Hack to allow t prefix on all commands. + if len(name) >= 2 && name[0] == 't' { + cmd.Thread = true + name = name[1:] + } + + // Hack to allow d prefix on all commands. + if len(name) >= 2 && name[0] == 'd' { + cmd.Delete = true + name = name[1:] + } + cmd.Name = name + + // Search command table. + for _, ct := range cmdtab { + if ct.Name == name { + if ct.Args == 0 && len(av) > 1 { + return nil, fmt.Errorf("%s doesn't take an argument", name) + } + cmd.F = ct.F + cmd.TF = ct.TF + if name == "m" { + // mute applies to all thread no matter what + cmd.Thread = true + } + return cmd, nil + } + } + return nil, fmt.Errorf("unknown command %s", name) +} + +func parseaddr(addr string, deflt int) (n int, targ *imap.MsgPart, rest string, err error) { + dot := dot + n = deflt + for { + old := addr + n, targ, rest, err = parseaddr1(addr, n, dot) + if targ != nil || rest == old || err != nil { + break + } + if n < 1 || n > len(msgs) { + return 0, nil, "", fmt.Errorf("message number %d out of range", n) + } + dot = &msgs[n-1].Root + addr = rest + } + return +} + +func parseaddr1(addr string, deflt int, dot *imap.MsgPart) (n int, targ *imap.MsgPart, rest string, err error) { + base := 0 + if dot != nil { + base = msgNum[dot.Msg] + 1 + } + if addr == "" { + return deflt, nil, addr, nil + } + var i int + sign := 0 + switch c := addr[0]; c { + case '+': + sign = +1 + addr = addr[1:] + case '-': + sign = -1 + addr = addr[1:] + case '.': + if base == 0 { + return 0, nil, "", fmt.Errorf("no message selected") + } + n = base + i = 1 + goto HaveNumber + case '$': + if len(msgs) == 0 { + return 0, nil, "", fmt.Errorf("no messages") + } + n = len(msgs) + i = 1 + goto HaveNumber + case '/', '?': + var re *regexp.Regexp + re, addr, err = parsere(addr) + if err != nil { + return + } + var delta int + if c == '/' { + delta = +1 + } else { + delta = -1 + } + for j := base + delta; 1 <= j && j <= len(msgs); j += delta { + if re.MatchString(header(msgs[j-1])) { + n = j + i = 0 // already cut addr + goto HaveNumber + } + } + err = fmt.Errorf("search") + return + // TODO case '%' + } + for i = 0; i < len(addr) && '0' <= addr[i] && addr[i] <= '9'; i++ { + n = 10*n + int(addr[i]) - '0' + } + if sign != 0 { + if n == 0 { + n = 1 + } + n = base + n*sign + goto HaveNumber + } + if i == 0 { + return deflt, nil, addr, nil + } +HaveNumber: + rest = addr[i:] + if i < len(addr) && addr[i] == '.' { + if n < 1 || n > len(msgs) { + err = fmt.Errorf("message number %d out of range", n) + return + } + targ = &msgs[n-1].Root + for i < len(addr) && addr[i] == '.' { + i++ + var j int + n = 0 + for j = i; j < len(addr) && '0' <= addr[j] && addr[j] <= '9'; j++ { + n = 10*n + int(addr[j]) - '0' + } + if j == i { + err = fmt.Errorf("malformed message number %s", addr[:j]) + return + } + if n < 1 || n > len(targ.Child) { + err = fmt.Errorf("message number %s out of range", addr[:j]) + return + } + targ = targ.Child[n-1] + i = j + } + n = 0 + rest = addr[i:] + return + } + return +} + +func parsere(addr string) (re *regexp.Regexp, rest string, err error) { + prog, rest, err := parseprog(addr) + if err != nil { + return + } + re, err = regexp.Compile(prog) + return +} + +var lastProg string + +func parseprog(addr string) (prog string, rest string, err error) { + if len(addr) == 1 { + if lastProg != "" { + return lastProg, "", nil + } + err = fmt.Errorf("no search") + return + } + i := strings.Index(addr[1:], addr[:1]) + if i < 0 { + prog = addr[1:] + rest = "" + } else { + i += 1 // adjust for slice in IndexByte arg + prog, rest = addr[1:i], addr[i+1:] + } + lastProg = prog + return +} + +func bcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + var m *imap.Msg + if dot == nil { + if len(msgs) == 0 { + return nil + } + m = msgs[0] + } else { + m = dot.Msg + } + for i := 0; i < 10; i++ { + hcmd(c, &m.Root) + next := nextMsg(m) + if next == nil { + break + } + m = next + } + return &m.Root +} + +func dcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + fmt.Fprintf(bout, "!address\n") + return nil + } + deleted[dot.Msg] = true + return &dot.Msg.Root +} + +func tdcmd(c *Cmd, msgs []*imap.Msg) *imap.MsgPart { + if len(msgs) == 0 { + fmt.Fprintf(bout, "!address\n") + return nil + } + for _, m := range msgs { + deleted[m] = true + } + return &msgs[len(msgs)-1].Root +} + +func ucmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + fmt.Fprintf(bout, "!address\n") + return nil + } + delete(deleted, dot.Msg) + return &dot.Msg.Root +} + +func eqcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + fmt.Fprintf(bout, "0") + } else { + fmt.Fprintf(bout, "%d", msgNum[dot.Msg]+1) + if dot != &dot.Msg.Root { + fmt.Fprintf(bout, ".%s", dot.ID) + } + } + fmt.Fprintf(bout, "\n") + return nil +} + +func from(h *imap.MsgHdr) string { + if len(h.From) < 1 { + return "?" + } + if name := h.From[0].Name; name != "" { + return name + } + return h.From[0].Email +} + +func header(m *imap.Msg) string { + var t string + if time.Now().Sub(m.Date) > 365*24*time.Hour { + t = m.Date.Format("01/02 15:04") + } else { + t = m.Date.Format("01/02 2006 ") + } + ch := ' ' + if len(m.Root.Child) > 1 || len(m.Root.Child) == 1 && len(m.Root.Child[0].Child) > 0 { + ch = 'H' + } + del := ' ' + if deleted[m] { + del = 'd' + } + return fmt.Sprintf("%-3d %c%c %s %-*.*s %.*s", + msgNum[m]+1, ch, del, t, + maxfrom, maxfrom, from(m.Hdr), + subjlen, m.Hdr.Subject) +} + +func hcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot != nil { + fmt.Fprintf(bout, "%s\n", header(dot.Msg)) + } + return nil +} + +func helpcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + fmt.Fprint(bout, "Commands are of the form [] [args]\n") + fmt.Fprint(bout, " := | ','| 'g'\n") + fmt.Fprint(bout, " := '.' | '$' | '^' | | | '+' | '-'\n") + fmt.Fprint(bout, " := '/''/' | '?''?'\n") + fmt.Fprint(bout, " :=\n") + for _, ct := range cmdtab { + fmt.Fprintf(bout, "%s\n", ct.Help) + } + return dot +} + +func mimecmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot != nil { + mimeH(fmt.Sprint(msgNum[dot.Msg]+1), dot) + } + return nil +} + +func mimeH(id string, p *imap.MsgPart) { + if p.ID != "" { + id = id + "." + p.ID + } + fmt.Fprintf(bout, "%s %s %s %#q %d\n", id, p.Type, p.Encoding+"/"+p.Charset, p.Name, p.Bytes) + for _, child := range p.Child { + mimeH(id, child) + } +} + +func icmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + sync(false) + return nil +} + +func ycmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + sync(true) + return nil +} + +func tpluscmd(c *Cmd, msgs []*imap.Msg) *imap.MsgPart { + if len(msgs) == 0 { + return nil + } + m := nextMsg(msgs[len(msgs)-1]) + if m == nil { + fmt.Fprintf(bout, "!no more messages\n") + return nil + } + return pcmd(c, &m.Root) +} + +func pluscmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + return nil + } + m := nextMsg(dot.Msg) + if m == nil { + fmt.Fprintf(bout, "!no more messages\n") + return nil + } + return pcmd(c, &m.Root) +} + +func addrlist(x []imap.Addr) string { + var b bytes.Buffer + for i, a := range x { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(a.String()) + } + return b.String() +} + +func wpcmd(w io.Writer, c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + return nil + } + if dot == &dot.Msg.Root { + h := dot.Msg.Hdr + if len(h.From) > 0 { + fmt.Fprintf(w, "From: %s\n", addrlist(h.From)) + } + fmt.Fprintf(w, "Date: %s\n", dot.Msg.Date) + if len(h.From) > 0 { + fmt.Fprintf(w, "To: %s\n", addrlist(h.To)) + } + if len(h.CC) > 0 { + fmt.Fprintf(w, "CC: %s\n", addrlist(h.CC)) + } + if len(h.BCC) > 0 { + fmt.Fprintf(w, "BCC: %s\n", addrlist(h.BCC)) + } + if len(h.Subject) > 0 { + fmt.Fprintf(w, "Subject: %s\n", h.Subject) + } + fmt.Fprintf(w, "\n") + } + printMIME(w, dot, true) + fmt.Fprintf(w, "\n") + return dot +} + +func pcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + defer bout.Flush() + return wpcmd(bout, c, dot) +} + +func pipecmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + args := c.Args[1:] + if len(args) == 0 { + fmt.Fprintf(bout, "!no command\n") + return dot + } + bout.Flush() + cmd := exec.Command(args[0], args[1:]...) + w, err := cmd.StdinPipe() + if err != nil { + fmt.Fprintf(bout, "!%s\n", err) + return dot + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + fmt.Fprintf(bout, "!%s\n", err) + return dot + } + wpcmd(w, c, dot) + w.Close() + if err := cmd.Wait(); err != nil { + fmt.Fprintf(bout, "!%s\n", err) + } + return dot +} + +func bangcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + args := c.Args[1:] + if len(args) == 0 { + fmt.Fprintf(bout, "!no command\n") + return dot + } + bout.Flush() + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(bout, "!%s\n", err) + } + return nil +} + +func unixfrom(h *imap.MsgHdr) string { + if len(h.From) == 0 { + return "" + } + return h.From[0].Email +} + +func unixtime(m *imap.Msg) string { + return dot.Msg.Date.Format("Mon Jan _2 15:04:05 MST 2006") +} + +func Pcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + return nil + } + if dot == &dot.Msg.Root { + fmt.Fprintf(bout, "From %s %s\n", + unixfrom(dot.Msg.Hdr), + unixtime(dot.Msg)) + } + bout.Write(dot.Raw()) + return dot +} + +func printMIME(w io.Writer, p *imap.MsgPart, top bool) { + switch { + case top && strings.HasPrefix(p.Type, "text/"): + text := p.ShortText() + if p.Type == "text/html" { + cmd := exec.Command("htmlfmt") + cmd.Stdin = bytes.NewBuffer(text) + if w == bout { + bout.Flush() + cmd.Stdout = os.Stdout + } else { + cmd.Stdout = w + } + if err := cmd.Run(); err != nil { + fmt.Fprintf(w, "%d.%s !%s\n", msgNum[p.Msg]+1, p.ID, err) + } + return + } + w.Write(text) + case p.Type == "text/plain": + if top { + panic("printMIME loop") + } + printMIME(w, p, true) + case p.Type == "multipart/alternative": + for _, pp := range p.Child { + if pp.Type == "text/plain" { + printMIME(w, pp, false) + return + } + } + if len(p.Child) > 0 { + printMIME(w, p.Child[0], false) + } + case strings.HasPrefix(p.Type, "multipart/"): + for _, pp := range p.Child { + printMIME(w, pp, false) + } + default: + fmt.Fprintf(w, "%d.%s !%s %s %s\n", msgNum[p.Msg]+1, p.ID, p.Type, p.Desc, p.Name) + } +} + +func qcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + flushDeleted() + xcmd(c, dot) + panic("not reached") +} + +type quoter struct { + bol bool + w io.Writer +} + +func (q *quoter) Write(b []byte) (n int, err error) { + n = len(b) + err = nil + for len(b) > 0 { + if q.bol { + q.w.Write([]byte("> ")) + q.bol = false + } + i := bytes.IndexByte(b, '\n') + if i < 0 { + i = len(b) + } else { + q.bol = true + i++ + } + q.w.Write(b[:i]) + b = b[i:] + } + return +} + +func quotecmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + return nil + } + m := dot.Msg + if len(m.Hdr.From) != 0 { + a := m.Hdr.From[0] + name := a.Name + if name == "" { + name = a.Email + } + date := m.Date.Format("Jan 2, 2006 at 15:04") + fmt.Fprintf(bout, "On %s, %s wrote:\n", date, name) + } + printMIME("er{true, bout}, dot, true) + return dot +} + +func addre(s string) string { + if len(s) < 4 || !strings.EqualFold(s[:4], "re: ") { + return "Re: " + s + } + return s +} + +func rcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil || dot.Msg.Hdr == nil { + fmt.Fprintf(bout, "!nothing to reply to\n") + return nil + } + + h := dot.Msg.Hdr + replyTo := h.ReplyTo + have := make(map[string]bool) + if len(replyTo) == 0 { + replyTo = h.From + } + if c.Name[0] == 'a' { + for _, a := range replyTo { + have[a.Email] = true + } + for _, a := range append(append(append([]imap.Addr(nil), h.From...), h.To...), h.CC...) { + if !have[a.Email] { + have[a.Email] = true + replyTo = append(replyTo, a) + } + } + } + if len(replyTo) == 0 { + fmt.Fprintf(bout, "!no one to reply to\n") + return dot + } + + args := []string{"-a", acct.Email, "-s", addre(h.Subject), "-in-reply-to", h.MessageID} + fmt.Fprintf(bout, "replying to:") + for _, a := range replyTo { + fmt.Fprintf(bout, " %s", a.Email) + args = append(args, "-to", a.String()) + } + for _, arg := range c.Args[1:] { + fmt.Fprintf(bout, " %s", arg) + args = append(args, "-to", arg) + } + fmt.Fprintf(bout, "\n") + bout.Flush() + cmd := exec.Command("gmailsend", args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + fmt.Fprintf(bout, "!%s\n", err) + } + return dot +} + +func fcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + fmt.Fprintf(bout, "!nothing to forward\n") + return nil + } + + return fwd(c, dot, nil) +} + +func tfcmd(c *Cmd, msgs []*imap.Msg) *imap.MsgPart { + if len(msgs) == 0 { + fmt.Fprintf(bout, "!nothing to forward\n") + return nil + } + + return fwd(c, &msgs[len(msgs)-1].Root, msgs) +} + +func fwd(c *Cmd, dot *imap.MsgPart, msgs []*imap.Msg) *imap.MsgPart { + addrs := c.Args[1:] + if len(addrs) == 0 { + fmt.Fprintf(bout, "!f command requires address to forward to\n") + return dot + } + + h := dot.Msg.Hdr + args := []string{"-a", acct.Email, "-s", "Fwd: " + h.Subject, "-append", "/dev/fd/3"} + fmt.Fprintf(bout, "forwarding to:") + for _, arg := range addrs { + fmt.Fprintf(bout, " %s", arg) + args = append(args, "-to", arg) + } + fmt.Fprintf(bout, "\n") + bout.Flush() + + cmd := exec.Command("gmailsend", args...) + r, w, err := os.Pipe() + if err != nil { + fmt.Fprintf(bout, "!%s\n", err) + return dot + } + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = []*os.File{r} + if err := cmd.Start(); err != nil { + r.Close() + fmt.Fprintf(bout, "!%s\n", err) + return dot + } + r.Close() + what := "message" + if len(msgs) > 1 { + what = "conversation" + } + fmt.Fprintf(w, "\n\n--- Forwarded %s ---\n", what) + if msgs == nil { + wpcmd(w, c, dot) + } else { + for _, m := range msgs { + wpcmd(w, c, &m.Root) + fmt.Fprintf(w, "\n\n") + } + } + w.Close() + if err := cmd.Wait(); err != nil { + fmt.Fprintf(bout, "!%s\n", err) + } + return dot +} + +func rethread() { + if !threaded { + sort.Sort(byUIDRev(msgs)) + } else { + byThread := make(map[uint64][]*imap.Msg) + for _, m := range msgs { + t := m.GmailThread + byThread[t] = append(byThread[t], m) + } + + var threadList [][]*imap.Msg + for _, t := range byThread { + sort.Sort(byUID(t)) + threadList = append(threadList, t) + } + sort.Sort(byUIDList(threadList)) + + msgs = msgs[:0] + for _, t := range threadList { + msgs = append(msgs, t...) + } + } + for i, m := range msgs { + msgNum[m] = i + } +} + +type byUID []*imap.Msg + +func (l byUID) Less(i, j int) bool { return l[i].UID < l[j].UID } +func (l byUID) Len() int { return len(l) } +func (l byUID) Swap(i, j int) { l[i], l[j] = l[j], l[i] } + +type byUIDRev []*imap.Msg + +func (l byUIDRev) Less(i, j int) bool { return l[i].UID > l[j].UID } +func (l byUIDRev) Len() int { return len(l) } +func (l byUIDRev) Swap(i, j int) { l[i], l[j] = l[j], l[i] } + +type byUIDList [][]*imap.Msg + +func (l byUIDList) Less(i, j int) bool { return l[i][len(l[i])-1].UID > l[j][len(l[j])-1].UID } +func (l byUIDList) Len() int { return len(l) } +func (l byUIDList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } + +func subj(m *imap.Msg) string { + s := m.Hdr.Subject + for strings.HasPrefix(s, "Re: ") || strings.HasPrefix(s, "RE: ") { + s = s[4:] + } + return s +} + +func mcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + c.ArgLine = "Muted" + scmd(c, dot) + return dcmd(c, dot) +} + +func tmcmd(c *Cmd, msgs []*imap.Msg) *imap.MsgPart { + c.ArgLine = "Muted" + tscmd(c, msgs) + return tdcmd(c, msgs) +} + +func scmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + return nil + } + return tscmd(c, []*imap.Msg{dot.Msg}) +} + +func tscmd(c *Cmd, msgs []*imap.Msg) *imap.MsgPart { + if len(msgs) == 0 { + return nil + } + arg := c.ArgLine + dot := &msgs[len(msgs)-1].Root + if arg == "" { + fmt.Fprintf(bout, "!s needs mailbox (label) name as argument\n") + return dot + } + if strings.EqualFold(arg, "Muted") { + if err := dot.Msg.Box.Mute(msgs); err != nil { + fmt.Fprintf(bout, "!mute: %s\n", err) + } + } else { + dst := dot.Msg.Box.Client.Box(arg) + if dst == nil { + fmt.Fprintf(bout, "!unknown mailbox %#q", arg) + return dot + } + if err := dst.Copy(msgs); err != nil { + fmt.Fprintf(bout, "!s %#q: %s\n", arg, err) + } + } + return dot +} + +func Wcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + if dot == nil { + return nil + } + if !isGmail { + fmt.Fprintf(bout, "!cmd W requires gmail\n") + return dot + } + url := fmt.Sprintf("https://mail.google.com/mail/b/%s/?shva=1#inbox/%x", acct.Email, dot.Msg.GmailThread) + if err := exec.Command("open", url).Run(); err != nil { + fmt.Fprintf(bout, "!%s\n", err) + } + return dot +} + +func xcmd(c *Cmd, dot *imap.MsgPart) *imap.MsgPart { + // TODO: remove saved attachments? + os.Exit(0) + panic("not reached") +} + +func flushDeleted() { + var toDelete []*imap.Msg + for m := range deleted { + toDelete = append(toDelete, m) + } + if len(toDelete) == 0 { + return + } + fmt.Fprintf(os.Stderr, "!deleting %d\n", len(toDelete)) + err := inbox.Delete(toDelete) + if err != nil { + fmt.Fprintf(os.Stderr, "!deleting: %s\n", err) + } +} + +func loadNew() { + if err := inbox.Check(); err != nil { + fmt.Fprintf(os.Stderr, "!inbox: %s\n", err) + } + + old := make(map[*imap.Msg]bool) + for _, m := range msgs { + old[m] = true + } + + nnew := 0 + new := inbox.Msgs() + for _, m := range new { + if old[m] { + delete(old, m) + } else { + msgs = append(msgs, m) + nnew++ + } + } + if nnew > 0 { + fmt.Fprintf(os.Stderr, "!%d new messages\n", nnew) + } + for m := range old { + // Deleted + m.Flags |= imap.FlagDeleted + delete(deleted, m) + } +} + +func sync(delete bool) { + if delete { + flushDeleted() + } + loadNew() + if delete { + w := 0 + for _, m := range msgs { + if !m.Deleted() { + msgs[w] = m + w++ + } + } + msgs = msgs[:w] + } + rethread() +} -- cgit v1.2.3-1-g7c22