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