Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cmd/cmdg/cmdg.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"sync"
"syscall"
"time"

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"

"github.com/ThomasHabets/cmdg/pkg/cmdg"
Expand Down Expand Up @@ -294,3 +297,21 @@ func main() {
log.Fatal(err)
}
}

func showPager(ctx context.Context, keys *input.Input, content string) error {
keys.Stop()
defer keys.Start()

cmd := exec.CommandContext(ctx, pagerBinary)
cmd.Stdin = strings.NewReader(content)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return errors.Wrapf(err, "failed to start pager %q", pagerBinary)
}
if err := cmd.Wait(); err != nil {
return errors.Wrapf(err, "pager %q failed", pagerBinary)
}
log.Infof("Pager finished")
return nil
}
98 changes: 97 additions & 1 deletion cmd/cmdg/view_messagelist.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package main

import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os/exec"
"slices"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -43,6 +48,7 @@ U — Mark marked mails as unread
s, ^s — Search
q — Quit
^L — Refresh screen
| — Pipe the selected messages to an external command in mbox format

Press [enter] to exit
`
Expand Down Expand Up @@ -430,7 +436,7 @@ func (mv *MessageView) Run(ctx context.Context) error {
log.Infof("Failed to parse mail date: %v", err)
tm = "???"
}
from, err := curmsg.GetFrom(ctx)
from, err := curmsg.GetFromNameOrAddress(ctx)
if err != nil {
return err
}
Expand Down Expand Up @@ -990,6 +996,48 @@ func (mv *MessageView) Run(ctx context.Context) error {
// stack frame on every navigation.
return nv.Run(ctx)
}
case "|":
cmds, err := dialog.Entry("Command> ", mv.keys)
if err == dialog.ErrAborted || cmds == "" {
// User aborted; do nothing.
break
} else if err != nil {
mv.errors <- errors.Wrap(err, "failed to get pipe command")
break
}

xedmsgs := make([]*cmdg.Message, 0)
for _, msg := range mv.messages {
if marked[msg.ID] {
xedmsgs = append(xedmsgs, msg)
}
}

if len(xedmsgs) == 0 {
log.Infof("No marked messages to do do operation %q on", "batch pipe")
break
}

cmd := exec.CommandContext(ctx, *shell, "-c", cmds)
in, err := cmd.StdinPipe()
if err != nil {
// needz a showPager here too?
mv.errors <- errors.Wrap(err, "failed to setup pipe")
break
}

go batchPipeHelper(ctx, xedmsgs, in, mv.errors)

buf := new(bytes.Buffer)
cmd.Stdout = buf
cmd.Stderr = buf
if err := cmd.Run(); err != nil {
mv.errors <- errors.Wrapf(err, "failed run pipe command: %q", cmds)
break
}
if err := showPager(ctx, mv.keys, buf.String()); err != nil {
mv.errors <- showPager(ctx, mv.keys, buf.String())
}
case "q":
return nil
default:
Expand Down Expand Up @@ -1032,3 +1080,51 @@ func (mv *MessageView) Run(ctx context.Context) error {
log.Debugf("Draw took %v", time.Since(st))
}
}

func batchPipeHelper(ctx context.Context, xedmsgs []*cmdg.Message, in io.WriteCloser, ec chan error) {

defer in.Close()
dest := bufio.NewWriter(in)
defer dest.Flush()

// TODO(rjk): This is a lot of code. Pull into a function.

// Messages need to be time-sorted to look like a mbox.
sorted := make([]SortableMessage, 0, len(xedmsgs))
for _, m := range xedmsgs {
t, err := m.GetTime(ctx)
if err != nil {
ec <- errors.Wrap(err, "failed to get date of message")
return
}
from, err := m.GetFromAddress(ctx)
if err != nil {
ec <- errors.Wrap(err, "failed to get from of message")
return
}
sorted = append(sorted, SortableMessage{msg: m, t: t, from: from})
}
slices.SortFunc(sorted, func(a, b SortableMessage) int { return a.t.Compare(b.t) })

for _, m := range sorted {
ms, err := m.msg.Raw(ctx)
if err != nil {
ec <- errors.Wrap(err, "failed to get raw message")
// Skip a message whose raw form cannot be read.
continue
}

if err := printRFC822FromLine(dest, m); err != nil {
ec <- errors.Wrap(err, "failed to write RFC822 header")
// Give up if we can't write to the pipe.
return
}

if _, err := dest.WriteString(ms); err != nil {
ec <- errors.Wrap(err, "failed to write raw email bodies to pipe")
// Give up if we can't write to the pipe.
return
}
}

}
25 changes: 3 additions & 22 deletions cmd/cmdg/view_openmessage.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"flag"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
Expand Down Expand Up @@ -181,7 +180,7 @@ func (ov *OpenMessageView) Draw(lines []string, scroll int) error {
scroll,
min(scroll+contentSpace, len(lines)),
len(lines),
min(100,int(100*float64(scroll+contentSpace) / float64(len(lines)))),
min(100, int(100*float64(scroll+contentSpace)/float64(len(lines)))),
searching,
)
line++
Expand Down Expand Up @@ -681,7 +680,7 @@ func (ov *OpenMessageView) Run(ctx context.Context) (*MessageViewOp, error) {
ov.errors <- errors.Wrapf(err, "failed run pipe command: %q", buf.String())
break
}
ov.errors <- ov.showPager(ctx, buf.String())
ov.errors <- showPager(ctx, ov.keys, buf.String())
case input.Backspace, input.CtrlH, input.PgUp, "Meta-v":
scroll = ov.scroll(ctx, len(lines), scroll, -(ov.screen.Height - 10))
ov.Draw(lines, scroll)
Expand All @@ -698,25 +697,7 @@ func (ov *OpenMessageView) showRaw(ctx context.Context) error {
if err != nil {
return errors.Wrapf(err, "Fetching raw msg")
}
return ov.showPager(ctx, m)
}

func (ov *OpenMessageView) showPager(ctx context.Context, content string) error {
ov.keys.Stop()
defer ov.keys.Start()

cmd := exec.CommandContext(ctx, pagerBinary)
cmd.Stdin = strings.NewReader(content)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return errors.Wrapf(err, "failed to start pager %q", pagerBinary)
}
if err := cmd.Wait(); err != nil {
return errors.Wrapf(err, "pager %q failed", pagerBinary)
}
log.Infof("Pager finished")
return nil
return showPager(ctx, ov.keys, m)
}

func (ov *OpenMessageView) scroll(ctx context.Context, lines, scroll, inc int) int {
Expand Down
19 changes: 17 additions & 2 deletions pkg/cmdg/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,9 @@ func (msg *Message) GetReplyToAll(ctx context.Context) (string, string, error) {
return from, strings.Join(filteredEmails(from, cc), ", "), err
}

// GetFrom returns email address (not name) of sender.
func (msg *Message) GetFrom(ctx context.Context) (string, error) {
// GetFromNameOrAddress returns the name (preferred) or address of the
// sender.
func (msg *Message) GetFromNameOrAddress(ctx context.Context) (string, error) {
s, err := msg.GetHeader(ctx, "From")
if err != nil {
return "", err
Expand All @@ -466,6 +467,20 @@ func (msg *Message) GetFrom(ctx context.Context) (string, error) {
return a.Address, nil
}

// GetFromAddress returns the address of the sender.
func (msg *Message) GetFromAddress(ctx context.Context) (string, error) {
s, err := msg.GetHeader(ctx, "From")
if err != nil {
return "", err
}
a, err := mail.ParseAddress(s)
if err != nil {
log.Warningf("%q is not a valid address: %v", s, err)
return "", err
}
return a.Address, nil
}

// Label is a gmail label.
type Label struct {
ID string
Expand Down