diff options
author | Christopher Speller <crspeller@gmail.com> | 2016-05-12 23:56:07 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-05-12 23:56:07 -0400 |
commit | 38ee83e45b4de7edf89bf9f0ef629eb4c6ad0fa8 (patch) | |
tree | a4fde09672192b97d453ad605b030bd5a10c5a45 /vendor/github.com/mattermost/rsc/google | |
parent | 84d2482ddbff9564c9ad75b2d30af66e3ddfd44d (diff) | |
download | chat-38ee83e45b4de7edf89bf9f0ef629eb4c6ad0fa8.tar.gz chat-38ee83e45b4de7edf89bf9f0ef629eb4c6ad0fa8.tar.bz2 chat-38ee83e45b4de7edf89bf9f0ef629eb4c6ad0fa8.zip |
Moving to glide
Diffstat (limited to 'vendor/github.com/mattermost/rsc/google')
-rw-r--r-- | vendor/github.com/mattermost/rsc/google/acme/Chat/main.go | 575 | ||||
-rw-r--r-- | vendor/github.com/mattermost/rsc/google/chat.go | 39 | ||||
-rw-r--r-- | vendor/github.com/mattermost/rsc/google/gmail/gmail.go | 1241 | ||||
-rw-r--r-- | vendor/github.com/mattermost/rsc/google/gmailsend/send.go | 370 | ||||
-rw-r--r-- | vendor/github.com/mattermost/rsc/google/googleserver/chat.go | 80 | ||||
-rw-r--r-- | vendor/github.com/mattermost/rsc/google/googleserver/main.go | 139 | ||||
-rw-r--r-- | vendor/github.com/mattermost/rsc/google/main.go | 181 |
7 files changed, 2625 insertions, 0 deletions
diff --git a/vendor/github.com/mattermost/rsc/google/acme/Chat/main.go b/vendor/github.com/mattermost/rsc/google/acme/Chat/main.go new file mode 100644 index 000000000..a161d8f10 --- /dev/null +++ b/vendor/github.com/mattermost/rsc/google/acme/Chat/main.go @@ -0,0 +1,575 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +/* +TODO: + - Del of main window should move to other window. + - Editing main window should update status on \n or something like that. + - Make use of full names from roster +*/ + +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "regexp" + "strings" + "time" + + "code.google.com/p/goplan9/plan9/acme" + "github.com/mattermost/rsc/google" + "github.com/mattermost/rsc/xmpp" +) + +var acmeDebug = flag.Bool("acmedebug", false, "print acme debugging") + +type Window struct { + *acme.Win // acme window + *acme.Event // most recent event received + err error // error reading event + typ string // kind of window "main", "chat" + name string // acme window title + remote string // for typ=="chat", remote address + dirty bool // window is dirty + blinky bool // window's dirty box is blinking + + lastTime time.Time +} + +type Msg struct { + w *Window // window where message belongs + *xmpp.Chat // recently received chat + err error // error reading chat message +} + +var ( + client *xmpp.Client // current xmpp client (can reconnect) + acct google.Account // google acct info + statusCache = make(map[string][]*xmpp.Presence) + active = make(map[string]*Window) // active windows + acmeChan = make(chan *Window) // acme events + msgChan = make(chan *Msg) // chat events + mainWin *Window + status = xmpp.Available + statusMsg = "" + lastActivity time.Time +) + +const ( + awayTime = 10 * time.Minute + extendedAwayTime = 30 * time.Minute +) + +func usage() { + fmt.Fprintf(os.Stderr, "usage: Chat [-a acct] name...\n") + flag.PrintDefaults() + os.Exit(2) +} + +var acctName = flag.String("a", "", "account to use") + +func main() { + flag.Usage = usage + flag.Parse() + + acct = google.Acct(*acctName) + + aw, err := acme.New() + if err != nil { + log.Fatal(err) + } + aw.Name("Chat/" + acct.Nick + "/") + + client, err = xmpp.NewClient("talk.google.com:443", acct.Email, acct.Password) + if err != nil { + log.Fatal(err) + } + + w := &Window{Win: aw, typ: "main", name: "Chat/" + acct.Nick + "/"} + data, err := ioutil.ReadFile(google.Dir() + "/chat." + acct.Nick) + if err != nil { + log.Fatal(err) + } + if err == nil { + w.Write("body", data) + } + mainWin = w + active[w.name] = w + go w.readAcme() + client.Roster() + setStatus(status) + go w.readChat() + lastActivity = time.Now() + + tick := time.Tick(0.5e9) +Loop: + for len(active) > 0 { + select { + case w := <-acmeChan: + if w == nil { + // Sync with reader. + continue + } + if w.err != nil { + if active[w.name] == nil { + continue + } + log.Fatal(w.err) + } + if *acmeDebug { + fmt.Fprintf(os.Stderr, "%s %c%c %d,%d %q\n", w.name, w.C1, w.C2, w.Q0, w.Q1, w.Text) + } + if w.C1 == 'M' || w.C1 == 'K' { + lastActivity = time.Now() + if status != xmpp.Available { + setStatus(xmpp.Available) + } + } + if (w.C2 == 'x' || w.C2 == 'X') && string(w.Text) == "Del" { + // TODO: Hangup connection for w.typ == "acct"? + delete(active, w.name) + w.Del(true) + continue Loop + } + + switch w.typ { + case "main": + switch w.C2 { + case 'L': // Button 3 in body: load chat window for contact. + w.expand() + fallthrough + case 'l': // Button 3 in tag + arg := string(w.Text) + showContact(arg) + continue Loop + } + case "chat": + if w.C1 == 'F' && w.C2 == 'I' { + continue Loop + } + if w.C1 != 'M' && w.C1 != 'K' { + break + } + if w.blinky { + w.blinky = false + w.Fprintf("ctl", "dirty\n") + } + switch w.C2 { + case 'X', 'x': + if string(w.Text) == "Ack" { + w.Fprintf("ctl", "clean\n") + } + case 'I': + w.sendMsg() + continue Loop + } + } + w.WriteEvent(w.Event) + + case msg := <-msgChan: + w := msg.w + if msg.err != nil { + w.Fprintf("body", "ERROR: %s\n", msg.err) + continue Loop + } + you := msg.Remote + if i := strings.Index(you, "/"); i >= 0 { + you = you[:i] + } + switch msg.Type { + case "chat": + w := showContact(you) + text := strings.TrimSpace(msg.Text) + if text == "" { + // Probably a composing notification. + continue + } + w.message("> %s\n", text) + w.blinky = true + w.dirty = true + + case "presence": + pr := msg.Presence + pr, new := savePresence(pr, you) + if !new { + continue + } + w := lookContact(you) + if w != nil { + w.status(pr) + } + mainStatus(pr, you) + } + + case t := <-tick: + switch status { + case xmpp.Available: + if t.Sub(lastActivity) > awayTime { + setStatus(xmpp.Away) + } + case xmpp.Away: + if t.Sub(lastActivity) > extendedAwayTime { + setStatus(xmpp.ExtendedAway) + } + } + for _, w := range active { + if w.blinky { + w.dirty = !w.dirty + if w.dirty { + w.Fprintf("ctl", "dirty\n") + } else { + w.Fprintf("ctl", "clean\n") + } + } + } + } + } +} + +func setStatus(st xmpp.Status) { + status = st + client.Status(status, statusMsg) + mainWin.statusTag(status, statusMsg) +} + +func savePresence(pr *xmpp.Presence, you string) (pr1 *xmpp.Presence, new bool) { + old := cachedPresence(you) + + pr.StatusMsg = strings.TrimSpace(pr.StatusMsg) + c := statusCache[you] + for i, p := range c { + if p.Remote == pr.Remote { + c[i] = pr + c[0], c[i] = c[i], c[0] + goto Best + } + } + c = append(c, pr) + c[0], c[len(c)-1] = c[len(c)-1], c[0] + statusCache[you] = c + +Best: + best := cachedPresence(you) + return best, old == nil || old.Status != best.Status || old.StatusMsg != best.StatusMsg +} + +func cachedPresence(you string) *xmpp.Presence { + c := statusCache[you] + if len(c) == 0 { + return nil + } + best := c[0] + for _, p := range c { + if p.Status > best.Status { + best = p + } + } + return best +} + +func short(st xmpp.Status) string { + switch st { + case xmpp.Unavailable: + return "?" + case xmpp.ExtendedAway: + return "x" + case xmpp.Away: + return "-" + case xmpp.Available: + return "+" + case xmpp.DoNotDisturb: + return "!" + } + return st.String() +} + +func long(st xmpp.Status) string { + switch st { + case xmpp.Unavailable: + return "unavailable" + case xmpp.ExtendedAway: + return "offline" + case xmpp.Away: + return "away" + case xmpp.Available: + return "available" + case xmpp.DoNotDisturb: + return "busy" + } + return st.String() +} + +func (w *Window) time() string { + /* + Auto-date chat windows: + + Show date and time on first message. + Show time if minute is different from last message. + Show date if day is different from last message. + + Oct 10 12:01 > hi + 12:03 hello there + 12:05 > what's up? + + 12:10 [Away] + */ + now := time.Now() + m1, d1, y1 := w.lastTime.Date() + m2, d2, y2 := now.Date() + w.lastTime = now + + if m1 != m2 || d1 != d2 || y1 != y2 { + return now.Format("Jan 2 15:04 ") + } + return now.Format("15:04 ") +} + +func (w *Window) status(pr *xmpp.Presence) { + msg := "" + if pr.StatusMsg != "" { + msg = ": " + pr.StatusMsg + } + w.message("[%s%s]\n", long(pr.Status), msg) + + w.statusTag(pr.Status, pr.StatusMsg) +} + +func (w *Window) statusTag(status xmpp.Status, statusMsg string) { + data, err := w.ReadAll("tag") + if err != nil { + log.Printf("read tag: %v", err) + return + } + //log.Printf("tag1: %s\n", data) + i := bytes.IndexByte(data, '|') + if i >= 0 { + data = data[i+1:] + } else { + data = nil + } + //log.Printf("tag2: %s\n", data) + j := bytes.IndexByte(data, '|') + if j >= 0 { + data = data[j+1:] + } + //log.Printf("tag3: %s\n", data) + + msg := "" + if statusMsg != "" { + msg = " " + statusMsg + } + w.Ctl("cleartag\n") + w.Write("tag", []byte(" "+short(status)+msg+" |"+string(data))) +} + +func mainStatus(pr *xmpp.Presence, you string) { + w := mainWin + if err := w.Addr("#0/^(.[ \t]+)?" + regexp.QuoteMeta(you) + "([ \t]*|$)/"); err != nil { + return + } + q0, q1, err := w.ReadAddr() + if err != nil { + log.Printf("ReadAddr: %s\n", err) + return + } + if err := w.Addr("#%d/"+regexp.QuoteMeta(you)+"/", q0); err != nil { + log.Printf("Addr2: %s\n", err) + } + q2, q3, err := w.ReadAddr() + if err != nil { + log.Printf("ReadAddr2: %s\n", err) + return + } + + space := " " + if q1 > q3 || pr.StatusMsg == "" { // already have or don't need space + space = "" + } + if err := w.Addr("#%d/.*/", q1); err != nil { + log.Printf("Addr3: %s\n", err) + } + w.Fprintf("data", "%s%s", space, pr.StatusMsg) + + space = "" + if q0 == q2 { + w.Addr("#%d,#%d", q0, q0) + space = " " + } else { + w.Addr("#%d,#%d", q0, q0+1) + } + w.Fprintf("data", "%s%s", short(pr.Status), space) +} + +func (w *Window) expand() { + // Use selection if any. + w.Fprintf("ctl", "addr=dot\n") + q0, q1, err := w.ReadAddr() + if err == nil && q0 <= w.Q0 && w.Q0 <= q1 { + goto Read + } + if err = w.Addr("#%d-/[a-zA-Z0-9_@.\\-]*/,#%d+/[a-zA-Z0-9_@.\\-]*/", w.Q0, w.Q1); err != nil { + log.Printf("expand: %v", err) + return + } + q0, q1, err = w.ReadAddr() + if err != nil { + log.Printf("expand: %v", err) + return + } + +Read: + data, err := w.ReadAll("xdata") + if err != nil { + log.Printf("read: %v", err) + return + } + w.Text = data + w.Q0 = q0 + w.Q1 = q1 + return +} + +// Invariant: in chat windows, the acme addr corresponds to the +// empty string just before the input being typed. Text before addr +// is the chat history (usually ending in a blank line). + +func (w *Window) message(format string, args ...interface{}) { + if *acmeDebug { + q0, q1, _ := w.ReadAddr() + log.Printf("message; addr=%d,%d", q0, q1) + } + if err := w.Addr(".-/\\n?\\n?/"); err != nil && *acmeDebug { + log.Printf("set addr: %s", err) + } + q0, _, _ := w.ReadAddr() + nl := "" + if q0 > 0 { + nl = "\n" + } + if *acmeDebug { + q0, q1, _ := w.ReadAddr() + log.Printf("inserting; addr=%d,%d", q0, q1) + } + w.Fprintf("data", nl+w.time()+format+"\n", args...) + if *acmeDebug { + q0, q1, _ := w.ReadAddr() + log.Printf("wrote; addr=%d,%d", q0, q1) + } +} + +func (w *Window) sendMsg() { + if *acmeDebug { + q0, q1, _ := w.ReadAddr() + log.Printf("sendMsg; addr=%d,%d", q0, q1) + } + if err := w.Addr(`.,./(.|\n)*\n/`); err != nil { + if *acmeDebug { + q0, q1, _ := w.ReadAddr() + log.Printf("no text (%s); addr=%d,%d", err, q0, q1) + } + return + } + q0, q1, _ := w.ReadAddr() + if *acmeDebug { + log.Printf("found msg; addr=%d,%d", q0, q1) + } + line, _ := w.ReadAll("xdata") + trim := string(bytes.TrimSpace(line)) + if len(trim) > 0 { + err := client.Send(xmpp.Chat{Remote: w.remote, Type: "chat", Text: trim}) + + // Select blank line before input (if any) and input. + w.Addr("#%d-/\\n?\\n?/,#%d", q0, q1) + if *acmeDebug { + q0, q1, _ := w.ReadAddr() + log.Printf("selected text; addr=%d,%d", q0, q1) + } + q0, _, _ := w.ReadAddr() + + // Overwrite with \nmsg\n\n. + // Leaves addr after final \n, which is where we want it. + nl := "" + if q0 > 0 { + nl = "\n" + } + errstr := "" + if err != nil { + errstr = fmt.Sprintf("\n%s", errstr) + } + w.Fprintf("data", "%s%s%s%s\n\n", nl, w.time(), trim, errstr) + if *acmeDebug { + q0, q1, _ := w.ReadAddr() + log.Printf("wrote; addr=%d,%d", q0, q1) + } + w.Fprintf("ctl", "clean\n") + } +} + +func (w *Window) readAcme() { + for { + e, err := w.ReadEvent() + if err != nil { + w.err = err + acmeChan <- w + break + } + //fmt.Printf("%c%c %d,%d %d,%d %#x %#q %#q %#q\n", e.C1, e.C2, e.Q0, e.Q1, e.OrigQ0, e.OrigQ1, e.Flag, e.Text, e.Arg, e.Loc) + w.Event = e + acmeChan <- w + acmeChan <- nil + } +} + +func (w *Window) readChat() { + for { + msg, err := client.Recv() + if err != nil { + msgChan <- &Msg{w: w, err: err} + break + } + //fmt.Printf("%s\n", *msg) + msgChan <- &Msg{w: w, Chat: &msg} + } +} + +func lookContact(you string) *Window { + return active["Chat/"+acct.Nick+"/"+you] +} + +func showContact(you string) *Window { + w := lookContact(you) + if w != nil { + w.Ctl("show\n") + return w + } + + ww, err := acme.New() + if err != nil { + log.Fatal(err) + } + + name := "Chat/" + acct.Nick + "/" + you + ww.Name(name) + w = &Window{Win: ww, typ: "chat", name: name, remote: you} + w.Fprintf("body", "\n") + w.Addr("#1") + w.OpenEvent() + w.Fprintf("ctl", "cleartag\n") + w.Fprintf("tag", " Ack") + if p := cachedPresence(you); p != nil { + w.status(p) + } + active[name] = w + go w.readAcme() + return w +} + +func randid() string { + return fmt.Sprint(time.Now()) +} diff --git a/vendor/github.com/mattermost/rsc/google/chat.go b/vendor/github.com/mattermost/rsc/google/chat.go new file mode 100644 index 000000000..8e9ae1c50 --- /dev/null +++ b/vendor/github.com/mattermost/rsc/google/chat.go @@ -0,0 +1,39 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package google + +import "github.com/mattermost/rsc/xmpp" + +type ChatID struct { + ID string + Email string + Status xmpp.Status + StatusMsg string +} + +type ChatSend struct { + ID *ChatID + Msg xmpp.Chat +} + +func (g *Client) ChatRecv(cid *ChatID) (*xmpp.Chat, error) { + var msg xmpp.Chat + if err := g.client.Call("goog.ChatRecv", cid, &msg); err != nil { + return nil, err + } + return &msg, nil +} + +func (g *Client) ChatStatus(cid *ChatID) error { + return g.client.Call("goog.ChatRecv", cid, &Empty{}) +} + +func (g *Client) ChatSend(cid *ChatID, msg *xmpp.Chat) error { + return g.client.Call("goog.ChatSend", &ChatSend{cid, *msg}, &Empty{}) +} + +func (g *Client) ChatRoster(cid *ChatID) error { + return g.client.Call("goog.ChatRoster", cid, &Empty{}) +} 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 [<range>] <command> [args]\n") + fmt.Fprint(bout, "<range> := <addr> | <addr>','<addr>| 'g'<search>\n") + fmt.Fprint(bout, "<addr> := '.' | '$' | '^' | <number> | <search> | <addr>'+'<addr> | <addr>'-'<addr>\n") + fmt.Fprint(bout, "<search> := '/'<gmail search>'/' | '?'<gmail search>'?'\n") + fmt.Fprint(bout, "<command> :=\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() +} diff --git a/vendor/github.com/mattermost/rsc/google/gmailsend/send.go b/vendor/github.com/mattermost/rsc/google/gmailsend/send.go new file mode 100644 index 000000000..ace6eb12b --- /dev/null +++ b/vendor/github.com/mattermost/rsc/google/gmailsend/send.go @@ -0,0 +1,370 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/base64" + "flag" + "fmt" + "io" + "net/smtp" + "os" + "regexp" + "strings" + + "github.com/mattermost/rsc/google" +) + +func enc(s string) string { + // TODO =? .. ?= + return s +} + +type Addr struct { + Name string + Email string +} + +func (a Addr) enc() string { + if a.Name == "" { + return "<" + a.Email + ">" + } + if a.Email == "" { + return enc(a.Name) + ":;" + } + return enc(a.Name) + " <" + a.Email + ">" +} + +type Addrs []Addr + +func (a *Addrs) String() string { + return "[addrlist]" +} + +func (a Addrs) has(s string) bool { + for _, aa := range a { + if aa.Email == s { + return true + } + } + return false +} + +func (a *Addrs) Set(s string) bool { + s = strings.TrimSpace(s) + if strings.HasSuffix(s, ">") { + j := strings.LastIndex(s, "<") + if j >= 0 { + *a = append(*a, Addr{strings.TrimSpace(s[:j]), s[j+1 : len(s)-1]}) + return true + } + } + + if strings.Contains(s, " ") { + fmt.Fprintf(os.Stderr, "invalid address: %s", s) + os.Exit(2) + } + *a = append(*a, Addr{"", s}) + return true +} + +func (a *Addrs) parseLine(s string) { + for _, f := range strings.Split(s, ",") { + f = strings.TrimSpace(f) + if f != "" { + a.Set(f) + } + } +} + +func (a Addrs) fixDomain() { + i := strings.Index(acct.Email, "@") + if i < 0 { + return + } + dom := acct.Email[i:] + for i := range a { + if a[i].Email != "" && !strings.Contains(a[i].Email, "@") { + a[i].Email += dom + } + } +} + +var from, to, cc, bcc, replyTo Addrs +var inReplyTo, subject string +var appendFile = flag.String("append", "", "file to append to end of body") + +var acct google.Account +var acctName = flag.String("a", "", "account to use") +var inputHeader = flag.Bool("i", false, "read additional header lines from stdin") + +func holdmode() { + if os.Getenv("TERM") == "9term" { + // forgive me + os.Stdout.WriteString("\x1B];*9term-hold+\x07") + } +} + +func match(line, prefix string, arg *string) bool { + if len(line) < len(prefix) || !strings.EqualFold(line[:len(prefix)], prefix) { + return false + } + *arg = strings.TrimSpace(line[len(prefix):]) + return true +} + +func main() { + flag.StringVar(&inReplyTo, "in-reply-to", "", "In-Reply-To") + flag.StringVar(&subject, "s", "", "Subject") + flag.Var(&from, "from", "From (can repeat)") + flag.Var(&to, "to", "To (can repeat)") + flag.Var(&cc, "cc", "CC (can repeat)") + flag.Var(&bcc, "bcc", "BCC (can repeat)") + flag.Var(&replyTo, "replyTo", "Reply-To (can repeat)") + + flag.Parse() + if flag.NArg() != 0 && !*inputHeader { + flag.Usage() + } + + var body bytes.Buffer + input := bufio.NewReader(os.Stdin) + if *inputHeader { + holdmode() + Loop: + for { + s, err := input.ReadString('\n') + if err != nil { + if err == io.EOF { + break Loop + } + fmt.Fprintf(os.Stderr, "reading stdin: %s\n", err) + os.Exit(2) + } + var arg string + switch { + default: + if ok, _ := regexp.MatchString(`^\S+:`, s); ok { + fmt.Fprintf(os.Stderr, "unknown header line: %s", s) + os.Exit(2) + } + body.WriteString(s) + break Loop + case match(s, "from:", &arg): + from.parseLine(arg) + case match(s, "to:", &arg): + to.parseLine(arg) + case match(s, "cc:", &arg): + cc.parseLine(arg) + case match(s, "bcc:", &arg): + bcc.parseLine(arg) + case match(s, "reply-to:", &arg): + replyTo.parseLine(arg) + case match(s, "subject:", &arg): + subject = arg + case match(s, "in-reply-to:", &arg): + inReplyTo = arg + } + } + } + + acct = google.Acct(*acctName) + from.fixDomain() + to.fixDomain() + cc.fixDomain() + bcc.fixDomain() + replyTo.fixDomain() + + smtpTo := append(append(to, cc...), bcc...) + + if len(from) == 0 { + // TODO: Much better + name := "" + email := acct.Email + if email == "rsc@swtch.com" || email == "rsc@google.com" { + name = "Russ Cox" + } + if email == "rsc@google.com" && (smtpTo.has("go@googlecode.com") || smtpTo.has("golang-dev@googlegroups.com") || smtpTo.has("golang-nuts@googlegroups.com")) { + from = append(from, Addr{name, "rsc@golang.org"}) + } else { + from = append(from, Addr{name, email}) + } + } + + if len(from) > 1 { + fmt.Fprintf(os.Stderr, "missing -from\n") + os.Exit(2) + } + + if len(to)+len(cc)+len(bcc) == 0 { + fmt.Fprintf(os.Stderr, "missing destinations\n") + os.Exit(2) + } + + if !*inputHeader { + holdmode() + } + _, err := io.Copy(&body, input) + if err != nil { + fmt.Fprintf(os.Stderr, "reading stdin: %s\n", err) + os.Exit(2) + } + + if *appendFile != "" { + f, err := os.Open(*appendFile) + if err != nil { + fmt.Fprintf(os.Stderr, "append: %s\n", err) + os.Exit(2) + } + _, err = io.Copy(&body, f) + f.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "append: %s\n", err) + os.Exit(2) + } + } + + var msg bytes.Buffer + fmt.Fprintf(&msg, "MIME-Version: 1.0\n") + if len(from) > 0 { + fmt.Fprintf(&msg, "From: ") + for i, a := range from { + if i > 0 { + fmt.Fprintf(&msg, ", ") + } + fmt.Fprintf(&msg, "%s", a.enc()) + } + fmt.Fprintf(&msg, "\n") + } + if len(to) > 0 { + fmt.Fprintf(&msg, "To: ") + for i, a := range to { + if i > 0 { + fmt.Fprintf(&msg, ", ") + } + fmt.Fprintf(&msg, "%s", a.enc()) + } + fmt.Fprintf(&msg, "\n") + } + if len(cc) > 0 { + fmt.Fprintf(&msg, "CC: ") + for i, a := range cc { + if i > 0 { + fmt.Fprintf(&msg, ", ") + } + fmt.Fprintf(&msg, "%s", a.enc()) + } + fmt.Fprintf(&msg, "\n") + } + if len(replyTo) > 0 { + fmt.Fprintf(&msg, "Reply-To: ") + for i, a := range replyTo { + if i > 0 { + fmt.Fprintf(&msg, ", ") + } + fmt.Fprintf(&msg, "%s", a.enc()) + } + fmt.Fprintf(&msg, "\n") + } + if inReplyTo != "" { + fmt.Fprintf(&msg, "In-Reply-To: %s\n", inReplyTo) + } + if subject != "" { + fmt.Fprintf(&msg, "Subject: %s\n", enc(subject)) + } + fmt.Fprintf(&msg, "Date: xxx\n") + fmt.Fprintf(&msg, "Content-Type: text/plain; charset=\"utf-8\"\n") + fmt.Fprintf(&msg, "Content-Transfer-Encoding: base64\n") + fmt.Fprintf(&msg, "\n") + enc64 := base64.StdEncoding.EncodeToString(body.Bytes()) + for len(enc64) > 72 { + fmt.Fprintf(&msg, "%s\n", enc64[:72]) + enc64 = enc64[72:] + } + fmt.Fprintf(&msg, "%s\n\n", enc64) + + auth := smtp.PlainAuth( + "", + acct.Email, + acct.Password, + "smtp.gmail.com", + ) + var smtpToEmail []string + for _, a := range smtpTo { + if a.Email != "" { + smtpToEmail = append(smtpToEmail, a.Email) + } + } + + if err := sendMail("smtp.gmail.com:587", auth, from[0].Email, smtpToEmail, msg.Bytes()); err != nil { + fmt.Fprintf(os.Stderr, "sending mail: %s\n", err) + os.Exit(2) + } +} + +/* + MIME-Version: 1.0 +Subject: commit/plan9port: rsc: 9term: hold mode back door +From: Bitbucket <commits-noreply@bitbucket.org> +To: plan9port-dev@googlegroups.com +Date: Tue, 11 Oct 2011 13:34:30 -0000 +Message-ID: <20111011133430.31146.55070@bitbucket13.managed.contegix.com> +Reply-To: commits-noreply@bitbucket.org +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +1 new changeset in plan9port: + +http://bitbucket.org/rsc/plan9port/changeset/8735d7708a1b/ +changeset: 8735d7708a1b +user: rsc +date: 2011-10-11 15:34:25 +summary: 9term: hold mode back door + +R=3Drsc +http://codereview.appspot.com/5248056 +affected #: 2 files (-1 bytes) + +Repository URL: https://bitbucket.org/rsc/plan9port/ + +-- + +This is a commit notification from bitbucket.org. You are receiving +this because you have the service enabled, addressing the recipient of +this email. + +*/ + +func sendMail(addr string, a smtp.Auth, from string, to []string, msg []byte) error { + c, err := smtp.Dial(addr) + if err != nil { + return err + } + if err = c.StartTLS(nil); err != nil { + return err + } + if err = c.Auth(a); err != nil { + return err + } + if err = c.Mail(from); err != nil { + return err + } + for _, addr := range to { + if err = c.Rcpt(addr); err != nil { + return err + } + } + w, err := c.Data() + if err != nil { + return err + } + _, err = w.Write(msg) + if err != nil { + return err + } + err = w.Close() + if err != nil { + return err + } + return c.Quit() +} diff --git a/vendor/github.com/mattermost/rsc/google/googleserver/chat.go b/vendor/github.com/mattermost/rsc/google/googleserver/chat.go new file mode 100644 index 000000000..8b064dc92 --- /dev/null +++ b/vendor/github.com/mattermost/rsc/google/googleserver/chat.go @@ -0,0 +1,80 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// TODO: Add ChatHangup. +// TODO: Auto-hangup chats that are gone. + +package main + +import ( + "fmt" + + "github.com/mattermost/rsc/google" + "github.com/mattermost/rsc/xmpp" +) + +type chatClient struct { + email string + id string + xmpp *xmpp.Client +} + +var chatClients = map[string]*chatClient{} + +func (*Server) chatClient(cid *google.ChatID) (*chatClient, error) { + id := cid.ID + cc := chatClients[cid.ID] + if cc == nil { + a := google.Cfg.AccountByEmail(cid.Email) + if a == nil { + return nil, fmt.Errorf("unknown account %s", cid.Email) + } + // New client. + cli, err := xmpp.NewClient("talk.google.com:443", a.Email, a.Password) + if err != nil { + return nil, err + } + cc = &chatClient{email: a.Email, id: id, xmpp: cli} + cc.xmpp.Status(cid.Status, cid.StatusMsg) + chatClients[id] = cc + } + return cc, nil +} + +func (srv *Server) ChatRecv(cid *google.ChatID, msg *xmpp.Chat) error { + cc, err := srv.chatClient(cid) + if err != nil { + return err + } + chat, err := cc.xmpp.Recv() + if err != nil { + return err + } + *msg = chat + return nil +} + +func (srv *Server) ChatStatus(cid *google.ChatID, _ *Empty) error { + cc, err := srv.chatClient(cid) + if err != nil { + return err + } + return cc.xmpp.Status(cid.Status, cid.StatusMsg) +} + +func (srv *Server) ChatSend(arg *google.ChatSend, _ *Empty) error { + cc, err := srv.chatClient(arg.ID) + if err != nil { + return err + } + return cc.xmpp.Send(arg.Msg) +} + +func (srv *Server) ChatRoster(cid *google.ChatID, _ *Empty) error { + cc, err := srv.chatClient(cid) + if err != nil { + return err + } + return cc.xmpp.Roster() +} diff --git a/vendor/github.com/mattermost/rsc/google/googleserver/main.go b/vendor/github.com/mattermost/rsc/google/googleserver/main.go new file mode 100644 index 000000000..2cd022446 --- /dev/null +++ b/vendor/github.com/mattermost/rsc/google/googleserver/main.go @@ -0,0 +1,139 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + // "flag" + "bufio" + "fmt" + "log" + "net" + "net/rpc" + "os" + "strings" + "syscall" + + "github.com/mattermost/rsc/google" + "github.com/mattermost/rsc/xmpp" +) + +func main() { + google.ReadConfig() + switch os.Args[1] { + case "add": + google.Cfg.Account = append(google.Cfg.Account, &google.Account{Email: os.Args[2], Password: os.Args[3]}) + google.WriteConfig() + case "serve": + serve() + case "accounts": + c, err := google.Dial() + if err != nil { + log.Fatal(err) + } + out, err := c.Accounts() + if err != nil { + log.Fatal(err) + } + for _, email := range out { + fmt.Printf("%s\n", email) + } + case "ping": + c, err := google.Dial() + if err != nil { + log.Fatal(err) + } + if err := c.Ping(); err != nil { + log.Fatal(err) + } + case "chat": + c, err := google.Dial() + if err != nil { + log.Fatal(err) + } + cid := &google.ChatID{ID: "1", Email: os.Args[2], Status: xmpp.Available, StatusMsg: ""} + go chatRecv(c, cid) + c.ChatRoster(cid) + b := bufio.NewReader(os.Stdin) + for { + line, err := b.ReadString('\n') + if err != nil { + log.Fatal(err) + } + line = line[:len(line)-1] + i := strings.Index(line, ": ") + if i < 0 { + log.Printf("<who>: <msg>, please") + continue + } + who, msg := line[:i], line[i+2:] + if err := c.ChatSend(cid, &xmpp.Chat{Remote: who, Type: "chat", Text: msg}); err != nil { + log.Fatal(err) + } + } + } +} + +func chatRecv(c *google.Client, cid *google.ChatID) { + for { + msg, err := c.ChatRecv(cid) + if err != nil { + log.Fatal(err) + } + switch msg.Type { + case "roster": + for _, contact := range msg.Roster { + fmt.Printf("%v\n", contact) + } + case "presence": + fmt.Printf("%v\n", msg.Presence) + case "chat": + fmt.Printf("%s: %s\n", msg.Remote, msg.Text) + default: + fmt.Printf("<%s>\n", msg.Type) + } + } +} + +func listen() net.Listener { + socket := google.Dir() + "/socket" + os.Remove(socket) + l, err := net.Listen("unix", socket) + if err != nil { + log.Fatal(err) + } + return l +} + +func serve() { + f, err := os.OpenFile(google.Dir()+"/log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) + if err != nil { + log.Fatal(err) + } + log.SetOutput(f) + syscall.Dup2(f.Fd(), 2) + os.Stdout = f + os.Stderr = f + l := listen() + rpc.RegisterName("goog", &Server{}) + rpc.Accept(l) + log.Fatal("rpc.Accept finished: server exiting") +} + +type Server struct{} + +type Empty google.Empty + +func (*Server) Ping(*Empty, *Empty) error { + return nil +} + +func (*Server) Accounts(_ *Empty, out *[]string) error { + var email []string + for _, a := range google.Cfg.Account { + email = append(email, a.Email) + } + *out = email + return nil +} diff --git a/vendor/github.com/mattermost/rsc/google/main.go b/vendor/github.com/mattermost/rsc/google/main.go new file mode 100644 index 000000000..b11a6eefb --- /dev/null +++ b/vendor/github.com/mattermost/rsc/google/main.go @@ -0,0 +1,181 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// TODO: Something about redialing. + +package google + +import ( + // "flag" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net" + "net/rpc" + "os" + "os/exec" + "syscall" + "time" +) + +func Dir() string { + dir := os.Getenv("HOME") + "/.goog" + st, err := os.Stat(dir) + if err != nil { + if err := os.Mkdir(dir, 0700); err != nil { + log.Fatal(err) + } + st, err = os.Stat(dir) + if err != nil { + log.Fatal(err) + } + } + if !st.IsDir() { + log.Fatalf("%s exists but is not a directory", dir) + } + if st.Mode()&0077 != 0 { + log.Fatalf("%s exists but allows group or other permissions: %#o", dir, st.Mode()&0777) + } + return dir +} + +func Dial() (*Client, error) { + socket := Dir() + "/socket" + c, err := net.Dial("unix", socket) + if err == nil { + return &Client{rpc.NewClient(c)}, nil + } + log.Print("starting server") + os.Remove(socket) + runServer() + for i := 0; i < 50; i++ { + c, err = net.Dial("unix", socket) + if err == nil { + return &Client{rpc.NewClient(c)}, nil + } + time.Sleep(200e6) + if i == 0 { + log.Print("waiting for server...") + } + } + return nil, err +} + +type Client struct { + client *rpc.Client +} + +type Empty struct{} + +func (g *Client) Ping() error { + return g.client.Call("goog.Ping", &Empty{}, &Empty{}) +} + +func (g *Client) Accounts() ([]string, error) { + var out []string + if err := g.client.Call("goog.Accounts", &Empty{}, &out); err != nil { + return nil, err + } + return out, nil +} + +func runServer() { + cmd := exec.Command("googleserver", "serve") + cmd.SysProcAttr = &syscall.SysProcAttr{} + if err := cmd.Start(); err != nil { + log.Fatal(err) + } +} + +type Config struct { + Account []*Account +} + +type Account struct { + Email string + Password string + Nick string +} + +func (cfg *Config) AccountByEmail(email string) *Account { + for _, a := range cfg.Account { + if a.Email == email { + return a + } + } + return nil +} + +var Cfg Config + +func ReadConfig() { + file := Dir() + "/config" + st, err := os.Stat(file) + if err != nil { + return + } + if st.Mode()&0077 != 0 { + log.Fatalf("%s exists but allows group or other permissions: %#o", file, st.Mode()&0777) + } + data, err := ioutil.ReadFile(file) + if err != nil { + log.Fatal(err) + } + Cfg = Config{} + if err := json.Unmarshal(data, &Cfg); err != nil { + log.Fatal(err) + } +} + +func WriteConfig() { + file := Dir() + "/config" + st, err := os.Stat(file) + if err != nil { + if err := ioutil.WriteFile(file, nil, 0600); err != nil { + log.Fatal(err) + } + st, err = os.Stat(file) + if err != nil { + log.Fatal(err) + } + } + if st.Mode()&0077 != 0 { + log.Fatalf("%s exists but allows group or other permissions: %#o", file, st.Mode()&0777) + } + data, err := json.MarshalIndent(&Cfg, "", "\t") + if err != nil { + log.Fatal(err) + } + if err := ioutil.WriteFile(file, data, 0600); err != nil { + log.Fatal(err) + } + st, err = os.Stat(file) + if err != nil { + log.Fatal(err) + } + if st.Mode()&0077 != 0 { + log.Fatalf("%s allows group or other permissions after writing: %#o", file, st.Mode()&0777) + } +} + +func Acct(name string) Account { + ReadConfig() + if name == "" { + if len(Cfg.Account) == 0 { + fmt.Fprintf(os.Stderr, "no accounts configured\n") + os.Exit(2) + } + return *Cfg.Account[0] + } + + for _, a := range Cfg.Account { + if a.Email == name || a.Nick == name { + return *a + } + } + fmt.Fprintf(os.Stderr, "cannot find account %#q", name) + os.Exit(2) + panic("not reached") +} |