edmail 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. #!/bin/sh
  2. # This file defines the edmail mail user agent.
  3. # Copyright (C) 2017-2018 mlaine@sdfeu.org
  4. # Permission is hereby granted, free of charge, to any person obtaining
  5. # a copy of this software and associated documentation files (the
  6. # "Software"), to deal in the Software without restriction, including
  7. # without limitation the rights to use, copy, modify, merge, publish,
  8. # distribute, sublicense, and/or sell copies of the Software, and to
  9. # permit persons to whom the Software is furnished to do so, subject to
  10. # the following conditions:
  11. # The above copyright notice and this permission notice shall be
  12. # included in all copies or substantial portions of the Software.
  13. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  14. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  15. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  16. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  17. # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  18. # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  19. # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  20. #-----------------------------------------------------------------------
  21. mon2num () {
  22. case "$mon" in
  23. 'Jan') mon=1 ;;
  24. 'Feb') mon=2 ;;
  25. 'Mar') mon=3 ;;
  26. 'Apr') mon=4 ;;
  27. 'May') mon=5 ;;
  28. 'Jun') mon=6 ;;
  29. 'Jul') mon=7 ;;
  30. 'Aug') mon=8 ;;
  31. 'Sep') mon=9 ;;
  32. 'Oct') mon=10 ;;
  33. 'Nov') mon=11 ;;
  34. 'Dec') mon=12 ;;
  35. esac
  36. }
  37. pad () {
  38. case "$1" in
  39. [0-9]) printf '0%s\n' "$1" ;;
  40. *) printf '%s\n' "$1" ;;
  41. esac
  42. }
  43. parsedate () {
  44. date="$(awk '/^Date: /{print;exit}' "$file" | cut -d' ' -f2-)"
  45. set -- $(printf '%s\n' "${date#*, }")
  46. day="${1#0}"; mon="$2"; year="$3"; time="${4%:*}"; tz="${5#*[+-]}"
  47. tz="$(printf '%s' "$tz" | awk '{print substr ($0,1,4)}')"
  48. hrs="${time%:*}"; hrs="${hrs#0}"
  49. min="${time#*:}"; min="${min#0}"
  50. tzhrs="${tz%??*}"; tzhrs="${tzhrs#0}"
  51. tzmin="${tz#*??}"; tzmin="${tzmin#0}"
  52. mon2num
  53. case "$5" in
  54. '+0000'|'-0000'|'GMT')
  55. true ;;
  56. '+'*)
  57. hrs="$((hrs-tzhrs))"
  58. min="$((min-tzmin))"
  59. test "$min" -lt 00 && min="$((min+60))" && hrs="$((hrs-1))"
  60. test "$hrs" -lt 00 && hrs="$((hrs+24))" && day="$((day-1))"
  61. case "$mon" in
  62. 3)
  63. test "$day" -lt 1 && day=28 && mon="$((mon-1))" ;;
  64. 5|7|10|12)
  65. test "$day" -lt 1 && day=30 && mon="$((mon-1))" ;;
  66. *)
  67. test "$day" -lt 1 && day=31 && mon="$((mon-1))" ;;
  68. esac ;;
  69. '-'*)
  70. hrs="$((hrs+tzhrs))"
  71. min="$((min+tzmin))"
  72. test "$min" -gt 59 && min="$((min%60))" && hrs="$((hrs+1))"
  73. test "$hrs" -gt 23 && hrs="$((hrs%24))" && day="$((day+1))"
  74. case "$mon" in
  75. 2)
  76. test "$day" -gt 28 && day=1 && mon="$((mon+1))" ;;
  77. 4|6|9|11)
  78. test "$day" -gt 30 && day=1 && mon="$((mon+1))" ;;
  79. *)
  80. test "$day" -gt 31 && day=1 && mon="$((mon+1))" ;;
  81. esac ;;
  82. *)
  83. hrs='??'; min='??' ;;
  84. esac
  85. test "$mon" -gt 12 && mon=1 && year="$((year+1))"
  86. test "$mon" -lt 1 && mon=12 && year="$((year-1))"
  87. mon="$(pad "$mon")"; day="$(pad "$day")"
  88. hrs="$(pad "$hrs")"; min="$(pad "$min")"
  89. }
  90. awkmatch () {
  91. if [ -n "$inverse" ]; then
  92. awk -v p="$pat" 'BEGIN{s=0}$0~p{s=1;exit}END{exit s}' "$file"
  93. else
  94. awk -v p="$pat" 'BEGIN{s=1}$0~p{s=0;exit}END{exit s}' "$file"
  95. fi
  96. }
  97. update () {
  98. if [ -n "$filter" ]; then
  99. true > "$templist"
  100. find "$dir" -type f |
  101. while read -r file; do
  102. if awkmatch; then
  103. printf '%s\n' "$file" >> "$templist"
  104. fi
  105. done
  106. else
  107. find "$dir" -type f > "$templist"
  108. fi
  109. if ! cmp -s "$templist" "$filelist"; then
  110. cp "$templist" "$filelist"
  111. true > "$statlist"
  112. while read -r file; do
  113. parsedate
  114. set -- $(wc < "$file")
  115. set -- "$file" "$1" "$3" "$year" "$mon" "$day" "$hrs" "$min"
  116. printf '%s\n' "$*" >> "$statlist"
  117. done < "$filelist"
  118. n="$(awk 'END{print NR}' "$filelist")"
  119. fi
  120. if [ -n "$bysize" ]; then
  121. sort -k 3,3nr -o "$statlist" "$statlist"
  122. elif ! cmp -s "$tempstat" "$statlist"; then
  123. sort -k 4,4n -k 5,5n -k 6,6n -k 7,7n -k 8,8n -o "$statlist" "$statlist"
  124. cp "$statlist" "$tempstat"
  125. fi
  126. }
  127. archive () {
  128. find "$new" -type f > "$templist"
  129. newn="$(awk 'END{print NR}' "$templist")"
  130. test "$newn" -eq 0 && return 1
  131. while read -r file; do
  132. filename="${file##*/}"
  133. mv "$file" "${cur}/${filename%:*}:2,S"
  134. done < "$templist"
  135. }
  136. loop () {
  137. i=0; j=0
  138. while read -r line; do
  139. i="$((i+1))"
  140. if [ "$cmd" = 'z' ]; then
  141. true
  142. elif [ "$i" -le "$upper" ]; then
  143. if [ "$i" -ge "$lower" ]; then
  144. j="$((j+1))"
  145. else
  146. continue
  147. fi
  148. else
  149. break
  150. fi
  151. "$1" "$line"
  152. done < "$statlist"
  153. }
  154. term () {
  155. cols="$(tput cols)"
  156. rows="$(tput lines)"
  157. rows="$((rows-3))"
  158. test "$rows" -lt 10 && rows=10
  159. upper="$((cursor+rows-6))"
  160. test "$upper" -gt "$n" && upper="$n"
  161. lower="$((upper-rows+1))"
  162. if [ "$lower" -lt 1 ]; then
  163. upper="$((upper-lower+1))"
  164. lower=1
  165. fi
  166. }
  167. field () {
  168. printf '%s\n' "$head" | awk -v p="^$1: " '$0~p{print;exit}' | cut -d' ' -f2-
  169. }
  170. brief () {
  171. set -- $1
  172. file="$1"; lines="$2"; bytes="$3"; date="$5/$6/$4 $7:$8"
  173. head="$(awk 'BEGIN{RS=""}NR==1{print;exit}' "$file")"
  174. case "$dir" in
  175. "$tmp"|"$record")
  176. name="$(field 'To')" ;;
  177. *)
  178. name="$(field 'From' | tr -d \")"; name="${name% <*}" ;;
  179. esac
  180. name="$(printf '%s\n' "$name" | cut -c-22)"
  181. subj="$(field 'Subject')"
  182. if [ "$lines" -lt 1000 ] && [ "$bytes" -lt 10000 ]; then
  183. size="$lines/$bytes"
  184. else
  185. size="$(du -h "$file" | cut -f1 )"
  186. fi
  187. pre=' '
  188. if [ "$cmd" = 'z' ]; then
  189. unset pre
  190. elif [ "$i" -eq "$cursor" ]; then
  191. pre='->'
  192. fi
  193. c="${#upper}"; wdth="$((cols-54-c))"
  194. set -- "$pre" "$c" "$i" "$name" "$date" "$size" "$wdth" "$subj"
  195. printf '%s%*s %-22s %s %-8s "%.*s"\n' "$@"
  196. }
  197. headline () {
  198. unset match sort
  199. test "$filter" && match="${inverse:+ *not*} matching \"$pat\""
  200. test "$bysize" && sort=', sorted by size'
  201. case "$n" in
  202. 0)
  203. printf '%s: no messages%s%s\n' "$dir" "${match:-}" "${sort:-}"
  204. cursor=1; return ;;
  205. 1)
  206. printf '%s: %s messages%s%s\n' "$dir" "$n" "${match:-}"\
  207. "${sort:-}" ;;
  208. *)
  209. printf '%s: %s messages%s%s\n' "$dir" "$n" "${match:-}"\
  210. "${sort:-}" ;;
  211. esac
  212. }
  213. list () {
  214. update; term
  215. if [ "$cmd" = 'z' ]; then
  216. wdth="$((wdth+2))"
  217. headline; loop 'brief' | pager
  218. else
  219. headline; loop 'brief'
  220. fi
  221. }
  222. part () {
  223. file="$(awk -v i="$cursor" 'NR==i{print $1;exit}' "$statlist")"
  224. head="$(awk 'BEGIN{RS=""}NR==1{print;exit}' "$file")"
  225. body="$(awk '/^$/?i++:i' "$file")"
  226. }
  227. retain () {
  228. date="Date: $(field 'Date')"
  229. addr="From: $(field 'From')"
  230. to="To: $(field 'To')"
  231. cc="$(field 'Cc')"
  232. subj="Subject: $(field 'Subject')"
  233. if [ -n "$cc" ]; then
  234. set -- "$date" "$addr" "$to" "Cc: $cc" "$subj"
  235. else
  236. set -- "$date" "$addr" "$to" "$subj"
  237. fi
  238. retain="$(printf '%s\n' "$@")"
  239. }
  240. page () {
  241. if [ "$n" -eq 0 ]; then
  242. printf 'EOF\n'
  243. return 1
  244. fi
  245. cursor="${cmd:-"$cursor"}"
  246. part
  247. retain
  248. tput clear
  249. printf '%s\n' "$retain" '' "$body" | pager
  250. }
  251. int () {
  252. if ([ "$1" -gt 0 ] && [ "$1" -le "$n" ]) 2>'/dev/null'; then
  253. return 0
  254. fi
  255. return 1
  256. }
  257. valid () {
  258. arg="${cmd#?}"
  259. if [ -n "$arg" ]; then
  260. lower="${arg%-*}"
  261. upper="${arg#*-}"
  262. if [ "$arg" = '*' ]; then
  263. lower=1
  264. upper="$n"
  265. elif [ "$arg" = '-.' ]; then
  266. lower=1
  267. upper="$cursor"
  268. elif [ "$arg" = '.-' ]; then
  269. lower="$cursor"
  270. upper="$n"
  271. elif [ "$arg" = ',' ]; then
  272. lower="$n"
  273. upper="$n"
  274. elif [ "$arg" = '-' ]; then
  275. printf '?\n'
  276. return 1
  277. else
  278. test -z "$upper" && upper="$n"
  279. test -z "$lower" && lower=1
  280. fi
  281. elif [ "$n" -eq 0 ]; then
  282. printf 'EOF\n'
  283. return 1
  284. else
  285. lower="$cursor"
  286. upper="$cursor"
  287. return 0
  288. fi
  289. if int "$lower" && int "$upper"; then
  290. case "$cmd" in
  291. 'd'*|'y'*|'~'*)
  292. if [ ! "$upper" -ge "$lower" ]; then
  293. printf 'invalid range\n'
  294. return 1
  295. fi ;;
  296. *)
  297. if [ "$lower" -ne "$upper" ]; then
  298. printf 'this command supports no ranges\n'
  299. return 1
  300. else
  301. cursor="$lower"
  302. fi ;;
  303. esac
  304. else
  305. printf '?\n'
  306. return 1
  307. fi
  308. }
  309. header () {
  310. valid || return 1
  311. part
  312. tput clear
  313. printf '%s\n' "$head" | pager
  314. }
  315. pipe () {
  316. valid || return 1
  317. part
  318. tput clear
  319. test "$args" || args="$pipeto"
  320. printf '%s\n' "$body" | sh -c "$args" | pager
  321. }
  322. cpdo () {
  323. file="${1%% *}"
  324. filename="${file##*/}"
  325. cp "$file" "$args/${filename%:*}:2,S"
  326. }
  327. copy () {
  328. valid || return 1
  329. case "$args" in
  330. '/'*)
  331. true ;;
  332. ''|'~')
  333. args="$HOME" ;;
  334. 'i')
  335. args="$new" ;;
  336. 'c')
  337. args="$cur" ;;
  338. 't')
  339. args="$tmp" ;;
  340. 'o')
  341. if [ -n "$record" ]; then
  342. args="$record"
  343. else
  344. args="$HOME/$args/o"
  345. fi ;;
  346. *)
  347. args="$HOME/$args" ;;
  348. esac
  349. if [ ! -d "$args" ]; then
  350. printf '%s does not exist, create it? [Y/n]: ' "$args"
  351. read -r mkd
  352. case "$mkd" in
  353. ''|'Y'|'y')
  354. mkdir -p "$args" ;;
  355. *)
  356. printf 'aborting\n'
  357. return 1 ;;
  358. esac
  359. fi
  360. loop 'cpdo'
  361. count="$((upper-lower+1))"
  362. if [ "$count" -eq 1 ]; then
  363. printf 'one message copied to %s\n' "$args"
  364. else
  365. printf '%s messages copied to %s\n' "$count" "$args"
  366. fi
  367. }
  368. printfile () {
  369. printf '%s\n' "${1%% *}" >> "$templist"
  370. }
  371. delete () {
  372. valid || return 1
  373. count="$((upper-lower+1))"
  374. true > "$templist"
  375. loop 'printfile'
  376. while read -r file; do
  377. filename="${file##*/}"
  378. printf '/%s/d\n%s\n' "$filename" 'wq' | ed -s "$filelist"
  379. printf '/%s/d\n%s\n' "$filename" 'wq' | ed -s "$statlist"
  380. if [ "$dir" = "$del" ]; then
  381. rm "$file"
  382. else
  383. mv "$file" "${del}/${filename%:*}:2,T"
  384. fi
  385. done < "$templist"
  386. cp "$statlist" "$tempstat"
  387. n="$(awk 'END{print NR}' "$filelist")"
  388. if [ "$lower" -lt "$cursor" ]; then
  389. if [ "$upper" -ge "$cursor" ]; then
  390. cursor="$lower"
  391. else
  392. cursor="$((cursor-upper+lower-1))"
  393. fi
  394. fi
  395. test "$cursor" -gt "$n" && cursor="$n"
  396. test "$cursor" -lt 1 && cursor=1
  397. if [ "$count" -gt 1 ]; then
  398. printf '%s messages deleted\n' "$count"
  399. fi
  400. }
  401. aliases () {
  402. test -f "$aliases" || return 1
  403. set -- $(printf '%s\n' "$to" | tr ',' ' ')
  404. for addr in "$@"; do
  405. local="${addr%@*}"
  406. if [ "$addr" = "$local" ]; then
  407. a="$(awk -v p="^$addr: " '$0~p{print;exit}' "$aliases" |
  408. cut -d':' -f2- | tr -d ' ')"
  409. else
  410. unset a
  411. fi
  412. shift; set -- $@ "${a:-"$addr"}"
  413. done
  414. to="$(printf '%s\n' '0a' "$*" '.' 'g/ /s//,/' 'g/,/s//, /' 'p' | ed -s)"
  415. }
  416. parsemsg () {
  417. mid="$(field 'Message-ID')"
  418. addr="$(field 'From')"
  419. subj="$(field 'Subject')"
  420. replyto="$(field 'Reply-To')"
  421. to="${replyto:-"$addr"}"
  422. to="${to#*<}"; to="${to%>*}"
  423. subj="Re: ${subj#Re: *}"
  424. ref="$(printf '%s\n' "$head" | awk '/:/{i=0}/^References:/{i=1}i')"
  425. irt="$(printf '%s\n' "$head" | awk '/:/{i=0}/^In-Reply-To:/{i=1}i')"
  426. irtl="$(printf '%s\n' "$irt" | awk 'END{print NR}')"
  427. }
  428. toed () {
  429. printf '%s\n' "$@" | ed -s "$msg"
  430. }
  431. hdrs () {
  432. h1="Message-ID: <${msg##*/}>"
  433. h2="Date: $(date '+%a, %d %b %Y %T %z')"
  434. h3="From: $from"
  435. h4="To: $to"
  436. h5="Subject: $subj"
  437. toed '0a' "$h1" "$h2" "$h3" "$h4" "$h5" '' '.' 'w'
  438. }
  439. reply () {
  440. valid || return 1
  441. part
  442. parsemsg
  443. printf 'To [%s]: ' "$to"
  444. read -r newto
  445. printf 'Subject [%s]: ' "$subj"
  446. read -r newsubj
  447. subj="${newsubj:-"$subj"}"
  448. to="${newto:-"$to"}"
  449. msg="${tmp}/$(date '+%s')-edmail|$user@$domain"
  450. printf '%s\n' "$body" > "$msg"
  451. toed 'g/^/s//> /' '0a' "$addr wrote:" '' '.' 'w'
  452. editor "$msg"
  453. test -f "$sign" && cat "$sign" >> "$msg"
  454. aliases
  455. hdrs
  456. if [ -n "$ref" ]; then
  457. toed '0a' "$ref" " $mid" '.' 'w'
  458. elif [ -n "$irt" ] && [ "$irtl" -eq 1 ]; then
  459. toed '0a' "References: ${irt#* }" " $mid" '.' 'w'
  460. fi
  461. if [ -n "$mid" ]; then
  462. toed '0a' "In-Reply-To: $mid" '.' 'w'
  463. fi
  464. }
  465. edit () {
  466. if [ "${dir##*/}" != 'tmp' ] && [ "$cmd" != '#' ]; then
  467. printf 'will not comply outside tmp/\n'
  468. return 1
  469. fi
  470. valid || return 1
  471. part
  472. if [ "$cmd" = '#' ]; then
  473. editor "$file"
  474. return 0
  475. fi
  476. to="$(field 'To')"
  477. subj="$(field 'Subject')"
  478. printf 'To [%s]: ' "$to"
  479. read -r newto
  480. printf 'Subject [%s]: ' "$subj"
  481. read -r newsubj
  482. subj="${newsubj:-"$subj"}"
  483. to="${newto:-"$to"}"
  484. msg="${tmp}/$(date '+%s')-edmail|$user@$domain"
  485. printf '%s\n' "$body" > "$msg"
  486. editor "$msg"
  487. aliases
  488. hdrs
  489. rm "$file"
  490. update
  491. }
  492. compose () {
  493. if [ -n "$args" ]; then
  494. to="$args"
  495. else
  496. printf 'To: '
  497. read -r to
  498. fi
  499. printf 'Subject: '
  500. read -r subj
  501. msg="${tmp}/$(date '+%s')-edmail|$user@$domain"
  502. editor "$msg"
  503. if [ -f "$msg" ]; then
  504. test -f "$sign" && cat "$sign" >> "$msg"
  505. aliases
  506. hdrs
  507. else
  508. printf 'nothing written\n'
  509. fi
  510. }
  511. send () {
  512. if [ -z "$(find "$tmp" -type f)" ]; then
  513. printf '%s contains no messages\n' "$tmp"
  514. return 1
  515. fi
  516. i=0; j=0
  517. for file in "$tmp"/*; do
  518. j="$((j+1))"
  519. addr="$(awk '/^To: /{print;exit}' "$file" | cut -d' ' -f2-)"
  520. if sendmail "$addr" < "$file"; then
  521. i="$((i+1))"
  522. if [ -n "$record" ]; then
  523. mv "$file" "$record"
  524. else
  525. rm "$file"
  526. fi
  527. elif [ "$sendmail" != 'null' ]; then
  528. printf 'edmail: could not send %s\n\n' "$file"
  529. fi
  530. done
  531. if [ "$i" -eq 0 ]; then
  532. printf 'no messages sent\n'
  533. elif [ "$j" -eq 1 ]; then
  534. printf '%s/%s message successfully sent\n' "$i" "$j"
  535. else
  536. printf '%s/%s messages successfully sent\n' "$i" "$j"
  537. fi
  538. test "${dir##*/}" = 'tmp' && update
  539. }
  540. seterr () {
  541. if [ "$args" = '-' ]; then
  542. printf 'cannot be unset\n'
  543. return 1
  544. fi
  545. }
  546. setopts () {
  547. if [ -z "$args" ]; then
  548. printf '%10s = %s\n'\
  549. fetchmail "$fetchmail"\
  550. sendmail "$sendmail"\
  551. folder "$maildir"\
  552. record "${record:-null}"\
  553. from "$from"\
  554. aliases "${aliases:-null}"\
  555. signature "${sign:-null}"\
  556. EDITOR "$editor"\
  557. PAGER "$pager"\
  558. cmd "${pipeto:-null}"\
  559. script "${script:-null}"
  560. return 0
  561. fi
  562. set -- $args
  563. option="$1"; shift
  564. if [ -z "$1" ]; then
  565. printf '?\n'
  566. return 1
  567. fi
  568. args="$*"
  569. case "$option" in
  570. 'fe'|'fetchmail')
  571. seterr || return 1
  572. exe "$args" && fetchmail="$args"
  573. fetchmail () { "$fetchmail"; } ;;
  574. 'se'|'sendmail')
  575. seterr || return 1
  576. exe "$args" && sendmail="$args"
  577. sendmail () { "$sendmail" "$@" || return 1; } ;;
  578. 'fo'|'folder')
  579. seterr || return 1
  580. if [ -d "$args" ]; then
  581. maildir="$args"
  582. else
  583. printf '%s %s\n' "$args is not a directory"
  584. fi ;;
  585. 're'|'record')
  586. case "$args" in
  587. '+'*) args="$maildir/${args#+}" ;;
  588. esac
  589. if [ -d "$args" ]; then
  590. record="$args"
  591. elif [ "$args" = '-' ]; then
  592. unset record
  593. else
  594. printf '%s %s\n' "$args is not a directory"
  595. fi ;;
  596. 'fr'|'from')
  597. seterr || return 1
  598. from="$args" ;;
  599. 'al'|'aliases')
  600. if [ -f "$args" ]; then
  601. aliases="$args"
  602. elif [ "$args" = '-' ]; then
  603. unset aliases
  604. else
  605. printf '%s is not a file\n' "$args"
  606. fi ;;
  607. 'si'|'signature')
  608. if [ -f "$args" ]; then
  609. sign="$args"
  610. elif [ "$args" = '-' ]; then
  611. unset sign
  612. else
  613. printf '%s is not a file\n' "$args"
  614. fi ;;
  615. 'ed'|'ED'|'EDITOR'|'editor')
  616. seterr || return 1
  617. exe "$args" && editor="$args"
  618. editor () { "$editor" "$@"; } ;;
  619. 'pa'|'PA'|'PAGER'|'pager')
  620. seterr || return 1
  621. exe "$args" && pager="$args"
  622. pager () { "$pager" "$@"; } ;;
  623. 'cm'|'cmd')
  624. if [ "$args" = '-' ]; then
  625. unset pipeto
  626. else
  627. pipeto="$args"
  628. fi ;;
  629. 'sc'|'script')
  630. if [ "$args" = '-' ]; then
  631. unset script
  632. script () { null; }
  633. else
  634. exe "$args" && script="$args"
  635. script () { "$script" "$@"; }
  636. fi ;;
  637. *)
  638. printf 'no such option\n' ;;
  639. esac
  640. }
  641. parserc () {
  642. val="$(printf '%s\n' 'g/ *$/s///' "/^set $1=" | ed -s "$mailrc"\
  643. 2>'/dev/null' | cut -d'=' -f2-)"
  644. case "$val" in
  645. '"'*|"'"*)
  646. val="${val#?}"
  647. val="${val%?}" ;;
  648. esac
  649. printf '%s\n' "$val"
  650. }
  651. exe () {
  652. for bin in "$@"; do
  653. if ! command -v "$bin" 1>'/dev/null'; then
  654. printf '%s is not executable\n' "$bin"
  655. return 1
  656. fi
  657. done
  658. }
  659. null () {
  660. printf 'no executable set\n'
  661. return 1
  662. }
  663. bins () {
  664. fetchmail="${fetchmail:-null}"
  665. sendmail="${sendmail:-null}"
  666. editor="${editor:-ed}"
  667. pager="${pager:-more}"
  668. script="${script:-null}"
  669. set -- "$fetchmail" "$sendmail" "$editor" "$pager" "$script"
  670. exe "$@" || return 1
  671. fetchmail () { "$fetchmail"; }
  672. sendmail () { "$sendmail" "$@" || return 1; }
  673. editor () { "$editor" "$@"; }
  674. pager () { "$pager" "$@"; }
  675. script () { "$script" "$@"; }
  676. }
  677. statfold () {
  678. for fold in "$@"; do
  679. if [ ! -d "$fold" ]; then
  680. if [ -z "$said" ]; then
  681. printf 'could not find the following, required, directories:\n'
  682. said=0
  683. fi
  684. printf ' %s\n' "$fold"
  685. notfound=0
  686. fi
  687. done
  688. test "$notfound" && return 0
  689. }
  690. define () {
  691. new="$maildir/new"
  692. cur="$maildir/cur"
  693. tmp="$maildir/tmp"
  694. del="$maildir/.del"
  695. user="${from##*<}"
  696. user="${user%@*}"
  697. domain="${from##*@}"
  698. domain="${domain%>*}"
  699. }
  700. init () {
  701. test "$1" && mailrc="$1"
  702. if [ ! -f "$mailrc" ]; then
  703. printf 'could not find %s\n' "$mailrc"
  704. return 1
  705. fi
  706. fetchmail="$(parserc 'fetchmail')"
  707. sendmail="$(parserc 'sendmail')"
  708. maildir="$(parserc 'folder')"
  709. record="$(parserc 'record')"
  710. from="$(parserc 'from')"
  711. aliases="$(parserc 'aliases')"
  712. sign="$(parserc 'signature')"
  713. editor="$(parserc 'EDITOR')"
  714. pager="$(parserc 'PAGER')"
  715. pipeto="$(parserc 'cmd')"
  716. script="$(parserc 'script')"
  717. bins || return 1
  718. if [ -z "$maildir" ]; then
  719. maildir="$HOME/.maildir"
  720. fi
  721. define
  722. set -- "$new" "$cur" "$tmp" "$del"
  723. if [ -n "$record" ]; then
  724. case "$record" in
  725. '+'*) record="$maildir/${record#+}" ;;
  726. esac
  727. set -- "$@" "$record"
  728. fi
  729. if [ -z "$from" ]; then
  730. printf "set the string option 'from' in %s\\n" "$mailrc"
  731. return 1
  732. elif statfold "$@"; then
  733. printf 'create them? [Y/n]: '
  734. read -r mkd
  735. case "$mkd" in
  736. ''|'Y'|'y')
  737. if mkdir -p "$@"; then
  738. printf '#\n' > "$templist"
  739. return 0
  740. fi ;;
  741. *)
  742. printf 'exiting\n' ;;
  743. esac
  744. else
  745. printf '#\n' > "$templist"
  746. return 0
  747. fi
  748. return 1
  749. }
  750. serv () {
  751. case "$cmd" in
  752. '')
  753. page ;;
  754. ',')
  755. cmd="$n"
  756. page ;;
  757. 'f'|'+')
  758. if [ "$cursor" -lt "$n" ]; then
  759. cursor="$((cursor+1))"
  760. cmd="$cursor"
  761. page
  762. else
  763. printf 'EOF\n'
  764. fi ;;
  765. 'b'|'-')
  766. if [ "$cursor" -gt 1 ]; then
  767. cursor="$((cursor-1))"
  768. cmd="$cursor"
  769. page
  770. else
  771. printf 'BOF\n'
  772. fi ;;
  773. 'l'|'.')
  774. list ;;
  775. 'z')
  776. list ;;
  777. 'g')
  778. cursor=1
  779. list ;;
  780. 'g'*)
  781. valid || return 1
  782. list ;;
  783. 'G')
  784. cursor="$n"
  785. list ;;
  786. 'k')
  787. k="$((cursor-20))"
  788. if [ "$k" -le 1 ]; then
  789. cursor=1
  790. else
  791. cursor="$k"
  792. fi
  793. list ;;
  794. 'j')
  795. j="$((cursor+20))"
  796. if [ "$j" -le "$n" ]; then
  797. cursor="$j"
  798. fi
  799. list ;;
  800. 'i')
  801. dir="$new"
  802. cursor=1
  803. list ;;
  804. 'c')
  805. dir="$cur"
  806. cursor=1
  807. list ;;
  808. 't')
  809. dir="$tmp"
  810. cursor=1
  811. list ;;
  812. '%')
  813. dir="$del"
  814. cursor=1
  815. list ;;
  816. 'o')
  817. if [ -n "$record" ]; then
  818. dir="$record"
  819. cursor=1
  820. list
  821. else
  822. printf '?\n'
  823. fi ;;
  824. 'p'*)
  825. if [ -n "$pipeto" ]; then
  826. pipe
  827. else
  828. printf '?\n'
  829. fi ;;
  830. 'm')
  831. compose ;;
  832. 'h'*)
  833. header ;;
  834. 'd'*)
  835. delete ;;
  836. 'r'*)
  837. reply ;;
  838. 'y'*)
  839. copy ;;
  840. 'e'*|'#'*)
  841. edit ;;
  842. 's')
  843. setopts ;;
  844. 'S')
  845. oldrc="$mailrc"
  846. init "$args" || init "$oldrc" ;;
  847. 'w')
  848. send ;;
  849. 'n')
  850. fetchmail
  851. test "$dir" = "$new" && update ;;
  852. 'v')
  853. printf 'edmail version 0.3.2\n' ;;
  854. 'a')
  855. if archive; then
  856. if [ "$newn" -eq 1 ]; then
  857. printf '1 new message moved to %s\n' "$cur"
  858. else
  859. printf '%s new messages moved to %s\n' "$newn" "$cur"
  860. fi
  861. else
  862. printf 'no messages to archive\n'
  863. fi
  864. test "$dir" = "$new" && update ;;
  865. 'q')
  866. find "$del" -type f -exec rm {} +
  867. archive; exit 0 ;;
  868. 'x')
  869. exit 0 ;;
  870. 'u')
  871. if [ -z "$bysize" ]; then
  872. bysize=0
  873. else
  874. unset bysize
  875. fi
  876. list ;;
  877. '/')
  878. if [ -z "$filter" ]; then
  879. filter=0
  880. else
  881. unset filter
  882. fi
  883. list ;;
  884. '/!'*)
  885. pat="${cmd#??}${args:+" $args"}"
  886. inverse=0
  887. filter=0
  888. cursor=1
  889. list ;;
  890. '/'*)
  891. pat="${cmd#?}${args:+" $args"}"
  892. unset inverse
  893. filter=0
  894. cursor=1
  895. list ;;
  896. '@')
  897. if [ -f "$aliases" ]; then
  898. pager "$aliases"
  899. else
  900. printf '?\n'
  901. fi ;;
  902. "'"*)
  903. valid || return 1
  904. file="$(awk -v i="$cursor" 'NR==i{print $1;exit}' "$statlist")"
  905. script "$file" "$args" ;;
  906. '~'*)
  907. valid || return 1
  908. awk -v a="$lower" -v b="$upper" 'NR>=a&&NR<=b{print $1}'\
  909. "$statlist" ;;
  910. ';')
  911. tput clear ;;
  912. '?')
  913. man edmail ;;
  914. '!'|'$')
  915. sh ;;
  916. '!'*)
  917. sh -c "${cmd#?} $args" ;;
  918. *)
  919. if int "$cmd"; then
  920. page
  921. else
  922. printf '?\n'
  923. fi ;;
  924. esac
  925. }
  926. main () {
  927. command -v mktemp 1>'/dev/null' || exit 1
  928. tempdir="$(mktemp -dt edmail.XXXXXX)"
  929. filelist="$tempdir/filelist"
  930. templist="$tempdir/templist"
  931. statlist="$tempdir/statlist"
  932. tempstat="$tempdir/tempstat"
  933. trap "rm -r $tempdir; exit" 0 1
  934. trap "printf '\n'; continue" 2
  935. mailrc="$HOME/.mailrc"
  936. test "$1" && mailrc="$1"
  937. init || exit 1
  938. cmd='i'; serv
  939. while true; do
  940. printf '& '
  941. read -r cmd args
  942. serv
  943. done
  944. }
  945. main "$@"