shithub: mother

Download patch

ref: 8130672a9e99e52ac95d3bc11d1adff7d3ffaccc
author: sl <sl@gaff>
date: Thu Aug 21 21:58:15 EDT 2025

initial import

--- /dev/null
+++ b/README
@@ -1,0 +1,65 @@
+mother
+======
+
+Mother is an [rc(1)](http://man.9front.org/1/rc) script that provides
+an experience similar to
+[nedmail(1)](http://man.9front.org/1/nedmail).
+
+Download it [here](http://only9fans.com/sl/mother/HEAD/info.html).
+
+help
+----
+
+	usage: mother [ -d ] [ -f mbox ] [ -p msg ] [ -r ]
+
+	Commands are of the form [<range>] <command> [args]
+	<range> := <addr> | <addr>,<addr>
+	<command> :=
+	a		reply to sender and recipients
+	b		print the next ten headers
+	d		mark for deletion
+	e ...		enter message (args passed to upas/marshal)
+	g/regexp/cmd	grep headlines for regexp and run cmd on matches
+	h		print message headline (,h for all)
+	help		print this help message
+	m ...		forward mail to address(es)
+	mb ...		change to specified mailbox
+	p		print the processed message
+	P		print the raw message
+	q		quit
+	r		reply to message
+	s ...		store message in specified mailbox
+	u		remove deletion mark
+	y		synchronize with mail box
+	"		print message in quoted form, suitable for reply
+	|cmd		pipe the processed message to a command
+	||cmd		pipe the raw message to a command
+	!cmd		run a command
+
+extensions
+----------
+
+A script may be used in conjunction with
+[faces(1)](http://man.9front.org/1/faces) and the
+[plumber(4)](http://man.9front.org/4/plumber) to open individual
+messages (or the entire mailbox) by clicking mb1 on a face in the
+[faces(1)](http://man.9front.org/1/faces) window.  Each click produces
+a new window containing the selected message.
+
+The script:
+[facemother](https://plan9.stanleylieber.com/rc/facemother) <- read
+and modify as needed before using
+
+The plumber rule:
+
+	# faces -> new mail window for message
+	type	is	text
+	data	matches	'[a-zA-Z¡-￿0-9_\-./]+'
+	data	matches	'/mail/fs/[a-zA-Z¡-￿0-9/]+/[0-9]+'
+	plumb	to	showmail
+	plumb	start	window -r 646 117 1366 676 facemother $0
+
+screenshots
+-----------
+
+![ello.co](img/ello.co.png)
--- /dev/null
+++ b/facemother
@@ -1,0 +1,19 @@
+#!/bin/rc
+# 2018-02-15T21:12:06-0500
+# helper program for launching http://plan9.stanleylieber.com/mother from faces(1).
+rfork en
+
+# plumber rules for faces(1)
+#type	is	text
+#data	matches	'[a-zA-Z¡-￿0-9_\-./]+'
+#data	matches	'/mail/fs/[a-zA-Z¡-￿0-9/]+/[0-9]+'
+#plumb	to	showmail
+#plumb	start	window -r 583 206 1303 1200 facemother $0
+
+# open and print only the indicated message
+msg=`{sed -n 19p $1/info}
+upas/fs -f /mail/box/sl/mbox/$msg
+mother -p 1
+
+# use existing mailbox, print the indicated message
+#mother -p `{basename $1}
binary files /dev/null b/img/ello.co.png differ
--- /dev/null
+++ b/index.md
@@ -1,0 +1,65 @@
+mother
+======
+
+Mother is an [rc(1)](http://man.9front.org/1/rc) script that provides
+an experience similar to
+[nedmail(1)](http://man.9front.org/1/nedmail).
+
+Download it [here](http://only9fans.com/sl/mother/HEAD/info.html).
+
+help
+----
+
+	usage: mother [ -d ] [ -f mbox ] [ -p msg ] [ -r ]
+
+	Commands are of the form [<range>] <command> [args]
+	<range> := <addr> | <addr>,<addr>
+	<command> :=
+	a		reply to sender and recipients
+	b		print the next ten headers
+	d		mark for deletion
+	e ...		enter message (args passed to upas/marshal)
+	g/regexp/cmd	grep headlines for regexp and run cmd on matches
+	h		print message headline (,h for all)
+	help		print this help message
+	m ...		forward mail to address(es)
+	mb ...		change to specified mailbox
+	p		print the processed message
+	P		print the raw message
+	q		quit
+	r		reply to message
+	s ...		store message in specified mailbox
+	u		remove deletion mark
+	y		synchronize with mail box
+	"		print message in quoted form, suitable for reply
+	|cmd		pipe the processed message to a command
+	||cmd		pipe the raw message to a command
+	!cmd		run a command
+
+extensions
+----------
+
+A script may be used in conjunction with
+[faces(1)](http://man.9front.org/1/faces) and the
+[plumber(4)](http://man.9front.org/4/plumber) to open individual
+messages (or the entire mailbox) by clicking mb1 on a face in the
+[faces(1)](http://man.9front.org/1/faces) window.  Each click produces
+a new window containing the selected message.
+
+The script:
+[facemother](https://plan9.stanleylieber.com/rc/facemother) <- read
+and modify as needed before using
+
+The plumber rule:
+
+	# faces -> new mail window for message
+	type	is	text
+	data	matches	'[a-zA-Z¡-￿0-9_\-./]+'
+	data	matches	'/mail/fs/[a-zA-Z¡-￿0-9/]+/[0-9]+'
+	plumb	to	showmail
+	plumb	start	window -r 646 117 1366 676 facemother $0
+
+screenshots
+-----------
+
+![ello.co](img/ello.co.png)
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,9 @@
+web:V:
+	cp README index.md
+	if(test -f mother.tgz)
+		rm mother.tgz
+	cd ..
+	tar zcvf /tmp/mother.tgz mother
+	cd mother
+	cp /tmp/mother.tgz ../src/
+	rm /tmp/mother.tgz
--- /dev/null
+++ b/mother
@@ -1,0 +1,504 @@
+#!/bin/rc
+# 2022-11-16T18:11:20-05:00
+# Mother wants to talk to you.
+# Similar to nedmail. Use with 9front or nupas/fs (creates mdir format files on save).
+# BONUS: Helper program for use with faces(1): http://plan9.stanleylieber.com/mother/facemother
+rfork en
+ramfs -p
+argv0=$0
+if(~ $#editor 0)
+	editor=hold
+if(~ $#pager 0)
+	pager=cat
+mb=mbox
+msg=()
+sort=-r
+fn d{
+	if(test $1 -le $#rposts && test -d $rposts($1)){
+		flag +D $1 &&
+		dposts=($dposts $1) ||
+		echo !delete $1 failed
+	}
+	if not
+		echo !address
+}
+fn deldposts{
+
+
+	ndel=()
+	for(i in $dposts){
+		if(test $i -le $#rposts && test -d $rposts($i)){
+			echo delete $mb $rposts($i) >/mail/fs/ctl &&
+			echo !deleted $i &&
+			ndel=($ndel $i) ||
+			echo !delete $i failed
+		}
+	}
+	echo !$#ndel messages deleted
+
+
+}
+fn e{
+	>/tmp/e &&
+	eval $editor /tmp/e &&
+	yn send &&
+	if(~ $yn y)
+		/bin/upas/marshal $* </tmp/e
+}
+fn fakefile{
+	if(! ~ $#file 0 || ~ $disp file inline){
+		file='(file,'$"file')'
+		fake=`{
+			if(~ $type *gif *GIF)
+				echo body.gif
+			if not if(~ $type *jpeg *JPEG *jpg *JPG)
+				echo body.jpg
+			if not if(~ $type *png *PNG)
+				echo body.png
+			if not if(~ $file *.*)
+				echo body.^`{echo $file | sed 's/(^.*\.|\)$)//g'}
+			if not
+				echo body
+		}
+		filepath=`{echo -n /mail/fs/$mb/^$1^/$fake}
+		size=`{du $1^/$fake}
+		echo !--- $1 $type $size(1) $file [$filepath]
+	}
+}
+fn flag{
+	if(~ $1 [\+][aDdfrSs] [-][aDdfrSs]){
+		echo $1 >$rposts($2)^/flags &&
+		puth $2 ||
+		echo !address
+	}
+}
+fn fmtd{
+	date=`{read}
+	switch($date(2)){
+	case Jan;	mo=1
+	case Feb;	mo=2
+	case Mar;	mo=3
+	case Apr;	mo=4
+	case May;	mo=5
+	case Jun;	mo=6
+	case Jul;	mo=7
+	case Aug;	mo=8
+	case Sep;	mo=9
+	case Oct;	mo=10
+	case Nov;	mo=11
+	case Dec;	mo=12
+	}
+	switch($date(3)){
+	case [0-9]
+		da=0^$date(3)
+	case *
+		da=$date(3)
+	}
+	switch($date(6)){
+	case `{date | awk '{print $6;}'}
+		ti=`{echo $date(4) | awk '{print substr($0,0,5);}'}
+	case *
+		ti=$date(6)
+	}
+	echo $mo/$da $ti
+}
+fn geth{
+	for(i in $*){
+		flags=`{sed -n 18p $rposts($i)^/info | sed 's/-//g'}
+		mime=`{
+			if(~ `{sed -n 7p $rposts($i)^/info} multipart*)
+				echo H
+		}
+		size=`{sed -n 17p $rposts($i)^/info}
+		date=`{sed -n 5p $rposts($i)^/info | fmtd}
+		from=`{sed -n 1p $rposts($i)^/info}
+		subject=`{sed -n 6p $rposts($i)^/info | awk '{print substr($0,0,50);}'}
+		# Unicode 00a0 divides the message number from the headline.
+		# Command input ignores everything after the unicode 00a0.
+		# These lines may be selected and sent to the prompt
+		# in order to print the indicated message.
+		echo '  '$"i' '$"mime' '$"flags'   '$"size'   '$"date' '$"from'	'$"subject
+	}
+}
+fn getposts{ ls | grep -e '^[0-9]+$' | sort -n $sort }
+fn getr{
+	switch($*){
+	case ,;	echo $posts
+	case ,*;	seq 1 `{echo $* | sed 's/,//g'}
+	case *,;	seq `{echo $* | sed 's/,//g'} $posts($#posts)
+	case *,*;	seq `{echo $* | sed 's/,/ /g'}
+	case *;	echo $*
+	}
+}
+fn h{ sed -n $1^p /tmp/h }
+fn m{
+	if(test $1 -le $#rposts && test -f $rposts($1)^/info){
+		subject=`{sed -n 6p $rposts($1)^/info}
+		if(! ~ $subject FWD:* Fwd:* fwd:*)
+			subject=(Fwd: $subject)
+		e -s $"subject -A $rposts($1)^/raw $*(2-) &&
+		flag +a $1
+	}
+	if not
+		echo !address
+}
+fn mb{
+	mb=$1
+	if(test -d /mail/box/$user/$mb || test -d $mb){
+		if(! ~ $mb mbox){
+			if(~ $mb /mail/fs/*)
+				mb=`{basename $mb}
+			if not if(~ $mb /*)
+				echo open $mb `{basename $mb} >/mail/fs/ctl
+			if not
+				echo open /mail/box/$user/$mb $mb >/mail/fs/ctl
+			mb=`{basename $mb}
+		}
+		cd /mail/fs/$mb
+		dposts=()
+		y
+		post=$posts(1)
+		prompt=$post
+	}
+	if not
+		echo !^$mb does not exist
+}
+fn printhelp{
+echo 'Commands are of the form [<range>] <command> [args]
+<range> := <addr> | <addr>','<addr>
+<command> :=
+a		reply to sender and recipients
+b		print the next ten headers
+d		mark for deletion
+e ...		enter message (args passed to upas/marshal)
+g/regexp/cmd	grep headlines for regexp and run cmd on matches
+h		print message headline (,h for all)
+help		print this help message
+m ...		forward mail to address(es)
+mb ...		change to specified mailbox
+p		print the processed message
+P		print the raw message
+q		quit
+r		reply to message
+s ...		store message in specified mailbox
+u		remove deletion mark
+y		synchronize with mail box
+"		print message in quoted form, suitable for reply
+|cmd		pipe the processed message to a command
+||cmd		pipe the raw message to a command
+!cmd		run a command'
+}
+fn pp{
+if(test $1 -le $#rposts && test -f $rposts($1)^/header){
+{ # Avoid stutter by dumping everything into a file first.
+	cat $rposts($1)^/header
+	echo
+	if(test -d $rposts($1)^/1){
+		parts=`{ls -p $rposts($1) | grep -e '^[0-9]+'}
+		body=1/body
+		if(test -f $rposts($1)^/1/1/body)
+			body=1/1/body
+	}
+	if not{
+		parts=()
+		body=body
+	}
+	type=`{file -m $rposts($1)^/$body}
+	if(~ $type text/plain)
+		cat $rposts($1)^/$body
+	if not if(~ $type text/html){
+		hcmd=(htmlfmt -l60 -cutf8 -a $rposts($1)^/$body)
+		echo !/bin/^$"hcmd
+		eval $hcmd
+		echo
+		echo !--- $rposts($1) $type `{du $rposts($1)^/$body | awk '{print $1}'} [file:///mail/fs/$mb/^$rposts($1)^/$body]	# plumb to browser
+	}
+	if not{
+		disp=`{sed -n 8p $rposts($1)^/info}
+		file=`{sed -n 9p $rposts($1)^/info}
+		fakefile $rposts($1)
+	}
+	echo
+	if(! ~ $#parts 0){
+		if(! ~ $#parts 1)
+			parts=$parts(2-)
+		for(j in $parts){
+			type=`{file -m $rposts($1)^/$j/body}
+			disp=`{sed -n 8p $rposts($1)^/$j/info}
+			file=`{sed -n 9p $rposts($1)^/$j/info}
+			fakefile $rposts($1)^/$j
+		}
+		parts=()
+	}
+} >/tmp/p
+	eval $pager /tmp/p
+	go=1
+	r=$1
+	post=$1
+	prompt=$1
+	flag +s $1
+}
+if not
+	echo !address
+}
+fn P{
+	if(test $1 -le $#rposts && test -f $rposts($1)^/rawunix){
+		eval $pager $rposts($1)^/rawunix
+		go=1
+		r=$1
+		post=$1
+		prompt=$1
+		flag +s $1
+	}
+	if not
+		echo !address
+}
+fn puth{
+	flags=`{sed -n 18p $rposts($1)^/info | sed 's/-//g'}
+	mime=`{
+		if(~ `{sed -n 7p $rposts($1)^/info} multipart*)
+			echo H
+	}
+	size=`{sed -n 17p $rposts($1)^/info}
+	date=`{sed -n 5p $rposts($1)^/info | fmtd}
+	from=`{sed -n 1p $rposts($1)^/info}
+	subject=`{sed -n 6p $rposts($1)^/info | awk '{print substr($0,0,50);}'}
+	{
+		echo $1
+		echo c
+		# REMEMBER: Unicode 00a0 divides the message number from the headline.
+		echo '  '^$1^' '$"mime' '$"flags'   '$"size'   '$"date' '$"from'	'$"subject
+		echo .
+		echo w
+		echo q
+	} | sam -d /tmp/h >/dev/null >[2=1]
+}
+fn r{
+	if(test $1 -le $#rposts && test -f $rposts($1)^/info){
+		subject=`{sed -n 6p $rposts($1)^/info}
+		if(! ~ $subject RE:* Re:* re:*)
+			subject=(Re: $subject)
+		e -R $rposts($1) -s $"subject $*(2-) `{sed -n 4p $rposts($1)^/info} &&
+		flag +a $1
+	}
+	if not
+		echo !address
+}
+fn s{
+	if(test $1 -le $#rposts && test -f $rposts($1)^/raw){
+		if(! test -d /mail/box/$user/$2)
+			echo create $2 >/mail/fs/ctl
+		/bin/upas/mbappend $2 $rposts($1)^/raw &&
+		flag +S $1 &&
+		echo !saved in $2
+	}
+	if not
+		echo !address
+}
+fn u{
+	if(test $1 -le $#rposts && test -d $rposts($1))
+		flag -D $1 || echo !undelete $1 failed
+	if not
+		echo !address
+	dposts=`{grep -v $1 <{for(j in $dposts){ echo $j }}}
+}
+fn y{
+	go=()
+	r=$post
+	if(! ~ $#dposts 0){
+		deldposts
+		dposts=()
+	}
+	if(! ~ $q 1){
+		rposts=`{getposts}
+		posts=`{seq 1 $#rposts}
+		post=$posts(1)
+		prompt=$post
+		geth $posts >/tmp/h
+		if(~ $#msg 0)
+			echo $#posts messages
+	}
+}
+fn yn{
+	echo
+	echo -n $* ' (y, n) '
+	yn=`{read}
+	switch($yn){
+	case y n
+		;
+	case *
+		yn
+	}
+}
+fn '"' { pager=cat pp $1 | sed 1d | sed 's/^/> /g' | sed 's/^> >/>>/g' }
+fn usage{
+	echo usage: $argv0 [ -b ] [ -d ] [ -f mbox ] [ -p msg ] [ -r ] >[1=2]
+	exit usage
+}
+while(~ $1 -*){
+	switch($1){
+	case -b;	biff=-b
+	case -d;	debug=1
+	case -f;	mb=$2; shift
+	case -p;	msg=$2; shift
+	case -r;	sort=(); shift
+	case *;	usage
+	}
+	shift
+}
+if(! ~ $#* 0)
+	usage
+if(! test -f /mail/fs/ctl)
+	/bin/upas/fs $biff #>[2]/dev/null
+if(! test -d /mail/box/$user/$mb && ! test -d $mb){
+	echo !^$mb does not exist
+	exit $mb^' does not exist'
+}
+mb $mb
+if(! ~ $#msg 0){
+	for(i in `{seq 1 $#rposts})
+		if(~ $msg $rposts($i))
+			msg=$posts($i)
+	pp $msg
+}
+while(){
+	echo -n $"prompt': '
+	# Command input ignores everything after unicode 00a0.
+	rcmd=`{read | sed 's/[ ].*$//g' | sed 's/^([0-9]+)?(,)?([0-9]+)?/& /g'}
+	switch($rcmd){
+	case ,* [0-9]*
+		r=`{getr $rcmd(1)}
+		cmd=$rcmd(2-)
+		if(~ $#cmd 0)
+			cmd=p
+	case *
+		r=$post
+		cmd=$rcmd
+	}
+	switch($cmd){
+	case a a' '*
+		for(i in $r)
+			r $i $cmd(2-) `{sed -n 2,3p $rposts($i)^/info | sort -n | uniq}
+		post=$r($#r)
+		prompt=$post
+	case b
+		r=`{seq $r(1) `{echo $r(1)^+10|bc}}
+		if(test $r($#r) -gt $posts($#posts))
+			r=`{seq $r(1) $posts($#posts)}
+		if(! ~ $#r 0 && test $r(1) -le $posts($#posts)){
+			sed -n $r(1)^,$r($#r)^p /tmp/h
+			post=$r($#r)
+			prompt=$post
+		}
+		if not
+			echo !address
+	case d
+		for(i in $r)
+			d $i
+		post=$r($#r)
+		prompt=$post
+	case e' '*
+		e $cmd(2-)
+	case g/*
+		regexp=`{echo $cmd | awk -F '/' '{print $2;}'} # BUG: / is stripped from regexp and cmd.
+		cmd=`{echo $cmd | awk -F '/' '{$1=""; $2=""; print;}'}
+		r=`{</tmp/h grep -e $"regexp | sed 's/ .*$//g'} # Strip everything after unicode 00a0.
+		if(~ $#cmd 0)
+			cmd=p
+		if(~ $cmd d h p P u '"'){
+			for(i in $r)
+				eval $cmd $i
+		}
+		if not if(~ $cmd a' '* m' '* r' '* s' '*){
+			for(i in $r)
+				eval $cmd(1) $i $cmd(2-)
+		}
+		if not
+			echo !illegal command
+		post=$r($#r)
+		prompt=$post
+	case h
+		{
+			for(i in $r)
+				h $i
+		} | eval $pager
+		post=$r($#r)
+		prompt=$post
+	case help
+		printhelp
+	case m' '*
+		for(i in $r)
+			m $i $cmd(2-)
+		post=$r($#r)
+		prompt=$post
+	case mb' '*
+		mb $cmd(2-)
+	case p
+		for(i in $r)
+			pp $i
+	case P
+		for(i in $r)
+			P $i
+	case q
+		q=1 y # BUP STOP
+		exit ''
+	case r r' '*
+		for(i in $r)
+			r $i $cmd(2-)
+		post=$r($#r)
+		prompt=$post
+	case s' '*
+
+
+		for(i in $r)
+			s $i $cmd(2-)
+
+
+		post=$r($#r)
+		prompt=$post
+	case u
+
+
+		for(i in $r)
+			u $i
+
+
+		post=$r($#r)
+		prompt=$post
+	case y
+		y
+	case '"'
+		for(i in $r)
+			'"' $i | eval $pager
+		post=$r($#r)
+		prompt=$post
+	case '?'
+		echo dposts: $dposts
+		echo rposts: $rposts
+		echo posts: $posts
+		echo post: $post
+		echo r: $r
+		if(! ~ $#mlpath 0)
+			echo mdir: $mlpath^/^`{sed -n 19p $r/info}
+		if(! ~ $#msg 0)
+			echo msg: $msg
+	case '|'*
+		for(i in $r)
+			pager=cat pp $i | eval `{echo $cmd | sed 's/^.//'}
+	case '||'*
+		for(i in $r)
+			pager=cat P $i | eval `{echo $cmd | sed 's/^..//'}
+	case '!'*
+		for(i in $r)
+			eval `{echo $cmd | sed 's/^.//'}
+	case *
+		if(~ $post $posts(1) && ~ $#go 0)
+			pp $post
+		if not if(! ~ $post $posts($#posts)){
+			post=`{echo $post^+1 | bc}
+			if(test $post -gt $posts($#posts))
+				post=$posts($#posts)
+			pp $post
+		}
+	}	
+}
--