1package main23import (4 "bufio"5 "errors"6 "fmt"7 "io/fs"8 "os"9 "os/exec"10 "regexp"11 "strings"12 "time"13)1415const (16 Flagged MailFlag = iota17 Unflagged18 Seen19 Unseen20 Trashed21 Untrashed22)2324const (25 // Output format used by mscan(1) (passed via the -f flag).26 mscanFmt = "%0R %19D <%0f> %0S"2728 // Maximum amount of characters to output for the from header.29 maxFrom = 1730)3132var (33 // POSIX extended regular expression for parsing 'mscanFmt'.34 mscanRegex = regexp.MustCompilePOSIX("^([^ ]+) ([0-9]+-[0-9]+-[0-9]+ [0-9][0-9]:[0-9][0-9]:[0-9][0-9]| *\\(unknown\\)) <([^>]+)> (.*)$")3536 // Workaround for https://github.com/leahneukirchen/mblaze/issues/26437 noMail = errors.New("mail no longer exists")38)3940type Mail struct {41 Path string42 Date time.Time43 From string44 Subject string45}4647// This is a workaround for a bug in mblaze. Presently, mblaze utilities do not check48// if the mail still exists if it is passed as an absolute file path, hence we do it here.49//50// See: https://github.com/leahneukirchen/mblaze/issues/26451func (m Mail) Exists() bool {52 // XXX: Could use syscall.access with F_OK here instead.53 _, err := os.Stat(m.Path)54 return !errors.Is(err, fs.ErrNotExist)55}5657func (m Mail) Show() error {58 // Use custom command-line options for less to ensure59 // the pager doesn't exit if the output fits on the screen.60 //61 // See also: https://github.com/leahneukirchen/mblaze/blob/v1.2/mshow.c#L818-L82262 pager := os.Getenv("PAGER")63 if pager == "" || strings.HasPrefix(pager, "less") {64 pager = "less --RAW-CONTROL-CHARS"65 }6667 if !m.Exists() {68 return noMail69 }70 cmd := exec.Command("mshow", m.Path)71 cmd.Env = append(os.Environ(), "MBLAZE_PAGER="+pager)7273 // Make sure that we use {stdout,stdin,stderr} of the parent74 // process. Need to this explicitly when using os/exec.75 cmd.Stdout = os.Stdout76 cmd.Stderr = os.Stderr77 cmd.Stdin = os.Stdin7879 return cmd.Run()80}8182func (m Mail) Reply() error {83 if !m.Exists() {84 return noMail85 }86 cmd := exec.Command("mrep", m.Path)8788 // Make sure that we use {stdout,stdin,stderr} of the parent89 // process. Need to this explicitly when using os/exec.90 cmd.Stdout = os.Stdout91 cmd.Stderr = os.Stderr92 cmd.Stdin = os.Stdin9394 return cmd.Run()95}9697func (m Mail) Flag(flag MailFlag) error {98 if !m.Exists() {99 return noMail100 }101 cmd := exec.Command("mflag", flag.CmdOpt(), m.Path)102 return cmd.Run()103}104105func (m Mail) String() string {106 from := m.From[0:min(len(m.From), maxFrom)]107108 var date string109 if m.Date.IsZero() {110 date = "(unknown)"111 } else {112 date = adaptiveTime(m.Date)113 }114115 out := fmt.Sprintf("%10s %17s %s", date, from, m.Subject)116 return out117}118119type MailFlag int120121func (f MailFlag) CmdOpt() string {122 switch f {123 case Unflagged:124 return "-f"125 case Flagged:126 return "-F"127 case Unseen:128 return "-s"129 case Seen:130 return "-S"131 case Untrashed:132 return "t"133 case Trashed:134 return "T"135 }136137 panic("unreachable")138}139140func mscan() ([]Mail, error) {141 var mails []Mail142143 cmd := exec.Command("mscan", "-f", mscanFmt, "1:-1")144 cmd.Env = append(os.Environ(), "MBLAZE_PAGER=")145 // TODO: Somehow configure mblaze to not strip at all.146 cmd.Env = append(cmd.Env, "COLUMNS=99999")147148 reader, err := cmd.StdoutPipe()149 if err != nil {150 return mails, err151 }152 defer reader.Close()153154 err = cmd.Start()155 if err != nil {156 return mails, err157 }158159 scanner := bufio.NewScanner(reader)160 for scanner.Scan() {161 subs := mscanRegex.FindStringSubmatch(scanner.Text())162 if subs == nil {163 // Message might have been moved since last mseq(1).164 // For example, if it was marked as seen via mflag(1).165 continue166 }167168 fp := subs[1]169 var date time.Time170 if strings.TrimSpace(subs[2]) == "(unknown)" {171 date = time.Time{} // TODO: Go doesn't have a Maybe monad :(172 } else {173 var err error174 date, err = time.Parse(time.DateTime, subs[2])175 if err != nil {176 return mails, err177 }178 }179 from := strings.TrimSpace(subs[3])180 subject := subs[4]181182 mails = append(mails, Mail{183 Path: fp,184 Date: date,185 From: from,186 Subject: subject,187 })188 }189190 err = cmd.Wait()191 if err != nil {192 return mails, err193 }194195 if len(mails) == 0 {196 return mails, fmt.Errorf("current sequence is empty")197 }198199 return mails, nil200}