info.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. package main
  2. import (
  3. "errors"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "io/ioutil"
  8. "math"
  9. "os"
  10. "strconv"
  11. "strings"
  12. "git.sr.ht/~blallo/papero/cli"
  13. "git.sr.ht/~blallo/papero/imaputils"
  14. "github.com/emersion/go-imap"
  15. )
  16. const maxUInt32 = int(^uint32(0))
  17. var infoVerbs cli.CommandMap
  18. func init() {
  19. infoVerbs = cli.CommandMap{
  20. "list-mailboxes": LsMailboxCmd{},
  21. // "list-subscribed": LsSubscribedCmd{},
  22. "list-messages": LsMsgCmd{},
  23. "display-message": CatMsgCmd{},
  24. }
  25. }
  26. type InfoCmd struct{}
  27. func (i InfoCmd) Func(args []string) error {
  28. flagset := flag.NewFlagSet(args[0], flag.ExitOnError)
  29. flagset.Usage = func() { i.Help(os.Stdout, flagset) }
  30. flagset.Parse(args[1:])
  31. subArgs := flagset.Args()
  32. if len(subArgs) == 0 {
  33. i.Help(os.Stderr, flagset)
  34. os.Exit(1)
  35. }
  36. cmd := subArgs[0]
  37. if Session.Info.Opts.Debug {
  38. Log.Debug("info verb:", cmd)
  39. }
  40. return infoVerbs[cmd].Func(subArgs)
  41. }
  42. func (i InfoCmd) Help(w io.Writer, set *flag.FlagSet) {
  43. fmt.Fprintf(w, "USAGE: %s info VERB [verb opts]\n", Session.Info.Name)
  44. fmt.Fprintf(w, "\nVERBS:\n")
  45. for verb := range infoVerbs {
  46. fmt.Fprintf(w, "\t%s\n", verb)
  47. }
  48. }
  49. // VERBS
  50. type LsMailboxCmd struct{}
  51. type mailboxInfo struct {
  52. name string
  53. attrs string
  54. }
  55. func (l LsMailboxCmd) Func(args []string) error {
  56. if Session.Info.Opts.Debug {
  57. Log.Debug("enter info list-mailboxes")
  58. }
  59. var withAttributes bool
  60. var output string
  61. var maxLen int
  62. var mailboxes []mailboxInfo
  63. flagset := flag.NewFlagSet(args[0], flag.ExitOnError)
  64. flagset.BoolVar(&withAttributes, "with-attrs", false, "toggle attributes display")
  65. flagset.Usage = func() { l.Help(os.Stdout, flagset) }
  66. flagset.Parse(args[1:])
  67. mboxInfo, err := imaputils.ListMailboxes(Session.Config, Session.Info.Opts.Debug)
  68. if err != nil {
  69. return err
  70. }
  71. if Session.Info.Opts.Debug {
  72. Log.Debug(mboxInfo)
  73. }
  74. for _, box := range mboxInfo {
  75. if withAttributes {
  76. if nameLen := len(box.Name); nameLen > maxLen {
  77. maxLen = nameLen
  78. }
  79. mailboxes = append(mailboxes, mailboxInfo{name: box.Name, attrs: fmt.Sprint(box.Attributes)})
  80. } else {
  81. output += fmt.Sprintln(box.Name)
  82. }
  83. }
  84. if maxLen == 0 {
  85. fmt.Print(output)
  86. } else {
  87. padFmtStr := fmt.Sprintf("%%-%dv\t", maxLen)
  88. for _, m := range mailboxes {
  89. line := fmt.Sprintf(padFmtStr, m.name)
  90. line += m.attrs
  91. fmt.Println(line)
  92. }
  93. }
  94. return nil
  95. }
  96. func (l LsMailboxCmd) Help(w io.Writer, set *flag.FlagSet) {
  97. fmt.Fprintf(w, "USAGE: %s info list-mailboxes [opts]\n", Session.Info.Name)
  98. fmt.Fprintf(w, "\nOPTS:\n")
  99. set.PrintDefaults()
  100. }
  101. type LsMsgCmd struct{}
  102. func (l LsMsgCmd) Func(args []string) error {
  103. if Session.Info.Opts.Debug {
  104. Log.Debug("enter info list-messages")
  105. }
  106. var limit uint
  107. var withSeq, withFrom, withDate, withFlags bool
  108. var sep string
  109. flagset := flag.NewFlagSet(args[0], flag.ExitOnError)
  110. flagset.StringVar(&sep, "sep", "\t", "separator between fields")
  111. flagset.UintVar(&limit, "limit", 0, "maximum number of messages to display (0 means no limit)")
  112. flagset.BoolVar(&withSeq, "seq", false, "show sequence number")
  113. flagset.BoolVar(&withFrom, "from", false, "show From address")
  114. flagset.BoolVar(&withDate, "date", false, "show message date")
  115. flagset.BoolVar(&withFlags, "flags", false, "show message flags")
  116. flagset.Usage = func() { l.Help(os.Stdout, flagset) }
  117. flagset.Parse(args[1:])
  118. subArgs := flagset.Args()
  119. if len(subArgs) != 1 {
  120. l.Help(os.Stderr, flagset)
  121. os.Exit(1)
  122. }
  123. msgs, err := imaputils.ListMessages(
  124. Session.Config,
  125. &imaputils.ListMessagesOpts{
  126. Mailbox: subArgs[0],
  127. Limit: uint32(limit),
  128. },
  129. Session.Info.Opts.Debug,
  130. )
  131. if err != nil {
  132. return err
  133. }
  134. printMsgs(msgs, withSeq, withFrom, withDate, withFlags, sep)
  135. return nil
  136. }
  137. func (l LsMsgCmd) Help(w io.Writer, set *flag.FlagSet) {
  138. fmt.Fprintf(w, "USAGE: %s info list-messages [opts] MAILBOX_NAME\n", Session.Info.Name)
  139. fmt.Fprintf(w, "\nOPTS:\n")
  140. set.PrintDefaults()
  141. }
  142. func numberSize(n uint32) int {
  143. return int(math.Floor(math.Log10(float64(n))))
  144. }
  145. func printMsgs(msgs []*imap.Message, withSeq, withFrom, withDate, withFlags bool, sep string) {
  146. var maxSeq, maxDateLen, maxFromLen, maxFlagsLen int
  147. var out, seqFmtStr, dateFmtStr, fromFmtStr, flagsFmtStr string
  148. for _, msg := range msgs {
  149. if seqSize := numberSize(msg.SeqNum); withSeq && seqSize > maxSeq {
  150. maxSeq = seqSize
  151. }
  152. if withDate {
  153. date := fmt.Sprint(msg.Envelope.Date)
  154. if dateLen := len(date); dateLen > maxDateLen {
  155. maxDateLen = dateLen
  156. }
  157. }
  158. if withFrom {
  159. for _, from := range msg.Envelope.From {
  160. if fromLen := len(from.Address()); fromLen > maxFromLen {
  161. maxFromLen = fromLen
  162. }
  163. }
  164. }
  165. if withFlags {
  166. var flags string
  167. for _, f := range msg.Flags {
  168. flags += fmt.Sprintf("%s ", f)
  169. }
  170. flags = strings.TrimRight(flags, " ")
  171. if flagsLen := len(flags); flagsLen > maxFlagsLen {
  172. maxFlagsLen = flagsLen
  173. }
  174. }
  175. }
  176. if withSeq {
  177. seqFmtStr = fmt.Sprintf("%%%dv%s", maxSeq, sep)
  178. }
  179. if withDate {
  180. dateFmtStr = fmt.Sprintf("%%-%dv%s", maxDateLen, sep)
  181. }
  182. if withFrom {
  183. fromFmtStr = fmt.Sprintf("%%-%dv%s", maxFromLen, sep)
  184. }
  185. if withFlags {
  186. flagsFmtStr = fmt.Sprintf("%%-%dv%s", maxFlagsLen, sep)
  187. }
  188. for _, msg := range msgs {
  189. var line string
  190. if withSeq {
  191. line += fmt.Sprintf(seqFmtStr, msg.SeqNum)
  192. }
  193. if withDate {
  194. line += fmt.Sprintf(dateFmtStr, msg.Envelope.Date)
  195. }
  196. if withFrom {
  197. line += fmt.Sprintf(fromFmtStr, msg.Envelope.From[0].Address())
  198. }
  199. if withFlags {
  200. var flags string
  201. for _, f := range msg.Flags {
  202. flags += fmt.Sprintf("%s ", f)
  203. }
  204. line += fmt.Sprintf(flagsFmtStr, strings.TrimRight(flags, " "))
  205. }
  206. line += msg.Envelope.Subject
  207. out += fmt.Sprintln(line)
  208. }
  209. fmt.Println(out)
  210. }
  211. type CatMsgCmd struct{}
  212. func (c CatMsgCmd) Func(args []string) error {
  213. if Session.Info.Opts.Debug {
  214. Log.Debug("enter info display-message")
  215. }
  216. var idList []uint32
  217. var withHeaders, withBody, withFlags, markRead bool
  218. flagset := flag.NewFlagSet(args[0], flag.ExitOnError)
  219. flagset.BoolVar(&withHeaders, "headers", false, "toggle headers display")
  220. flagset.BoolVar(&withBody, "no-body", false, "hide body")
  221. flagset.BoolVar(&withFlags, "flags", false, "show flags")
  222. flagset.BoolVar(&markRead, "seen", false, "mark as seen if not yet seen")
  223. flagset.Usage = func() { c.Help(os.Stdout, flagset) }
  224. flagset.Parse(args[1:])
  225. subArgs := flagset.Args()
  226. lenSubArgs := len(subArgs)
  227. if lenSubArgs < 2 {
  228. c.Help(os.Stderr, flagset)
  229. os.Exit(1)
  230. }
  231. mailbox := subArgs[0]
  232. if lenSubArgs > 1 {
  233. for _, id := range subArgs[1:] {
  234. mailId, err := parseToUint32(id)
  235. if err != nil {
  236. return err
  237. }
  238. idList = append(idList, mailId)
  239. }
  240. }
  241. opts := &imaputils.FetchOpts{
  242. Mailbox: mailbox,
  243. IdList: idList,
  244. WithHeaders: withHeaders,
  245. WithBody: !withBody,
  246. WithFlags: withFlags,
  247. Peek: !markRead,
  248. }
  249. if Session.Info.Opts.Debug {
  250. Log.Debug(opts)
  251. }
  252. messages, err := imaputils.FetchMessages(Session.Config, opts, Session.Info.Opts.Debug)
  253. if err != nil {
  254. return err
  255. }
  256. for _, m := range messages {
  257. err = printMessage(m, opts, withFlags)
  258. if err != nil {
  259. return err
  260. }
  261. }
  262. return nil
  263. }
  264. func (c CatMsgCmd) Help(w io.Writer, set *flag.FlagSet) {
  265. fmt.Fprintf(w, "USAGE: %s info display-message [opts] MAILBOX_NAME [MESSAGE_ID1 [MESSAGE_ID2 [...]]]\n", Session.Info.Name)
  266. fmt.Fprintf(w, "\nOPTS:\n")
  267. set.PrintDefaults()
  268. }
  269. var ErrOutOfBounds = errors.New("number is out of bounds")
  270. func parseToUint32(s string) (uint32, error) {
  271. out, err := strconv.Atoi(s)
  272. if err != nil {
  273. return 0, err
  274. }
  275. if out < 0 || out > maxUInt32 {
  276. return 0, ErrOutOfBounds
  277. }
  278. return uint32(out), nil
  279. }
  280. func printMessage(m *imap.Message, opts *imaputils.FetchOpts, withFlags bool) error {
  281. var out string
  282. if withFlags {
  283. if Session.Info.Opts.Debug {
  284. Log.Debug(m.Flags)
  285. }
  286. for _, f := range m.Flags {
  287. out += fmt.Sprintf("%s ", f)
  288. }
  289. out = strings.TrimRight(out, " ")
  290. out += "\n"
  291. }
  292. if opts.WithHeaders {
  293. out += fmt.Sprintln(formatAddresses(m.Envelope.From, "From"))
  294. out += fmt.Sprintln(formatAddresses(m.Envelope.Sender, "Sender"))
  295. out += fmt.Sprintln(formatAddresses(m.Envelope.Cc, "Cc"))
  296. out += fmt.Sprintln(formatAddresses(m.Envelope.Bcc, "Bcc"))
  297. out += fmt.Sprintln(formatAddresses(m.Envelope.ReplyTo, "ReplyTo"))
  298. out += fmt.Sprintf("InReplyTo: %s\n", m.Envelope.InReplyTo)
  299. out += fmt.Sprintf("Date: %v\n", m.Envelope.Date)
  300. out += fmt.Sprintf("Subject: %s\n", m.Envelope.Subject)
  301. headersSec := &imap.BodySectionName{Peek: opts.Peek, BodyPartName: imap.BodyPartName{Specifier: imap.HeaderSpecifier}}
  302. headersReader := m.GetBody(headersSec)
  303. headers, err := ioutil.ReadAll(headersReader)
  304. if err != nil {
  305. return err
  306. }
  307. out += fmt.Sprintln(string(headers))
  308. }
  309. if opts.WithBody {
  310. bodySec := &imap.BodySectionName{Peek: opts.Peek, BodyPartName: imap.BodyPartName{Specifier: imap.TextSpecifier}}
  311. bodyReader := m.GetBody(bodySec)
  312. body, err := ioutil.ReadAll(bodyReader)
  313. if err != nil {
  314. return err
  315. }
  316. out += fmt.Sprintln(string(body))
  317. }
  318. fmt.Println(out)
  319. return nil
  320. }
  321. func formatAddresses(addresses []*imap.Address, name string) string {
  322. out := fmt.Sprintf("%s: ", name)
  323. for _, address := range addresses {
  324. if address.PersonalName != "" {
  325. out += fmt.Sprintf("%s <%s>, ", address.PersonalName, address.Address())
  326. } else {
  327. out += fmt.Sprintf("%s, ", address.Address())
  328. }
  329. }
  330. return strings.TrimRight(out, ", ")
  331. }