#!/usr/skunk/bin/gawk -f #!/usr/bin/awk -f # @(#) editmbox.awk 1.0 94/01/30 # 92/08/31 john h. dubois iii (spcecdt@deeptht.armory.com) # 94/01/30 Fixed z command; was completely munged # 94/03/08 Use gawk so - options can be given BEGIN { NumFiles = ProcArgs(ARGC,ARGV,"ho:dumns",Options) - 1 if ("h" in Options || NumFiles != 1) { print \ "editmbox: edit a mailbox, removing unwanted header lines and deleting\n" \ "or saving messages.\n" \ "Usage: editmbox [-hmudns] [-o outfile] \n" \ "Output is put in .ed\n" \ "Warning: this program is fragile. If editing a large mailbox, save your\n"\ "work occasionally using the Checkpoint function, and don't ever hit your\n"\ "interrupt key!\n"\ "Options:\n" \ "h: print this help\n" \ "o: output is put in outfile instead of .ed\n" \ "m: don't complain about missing required fields in header\n" \ "u: don't complain about unrecognized fields in header\n" \ "d: don't discard any fields from header\n" \ "n: non-interactive: just discard unwanted fields from headers\n" \ "s: ignore EditStatus: field when reading in message file." exit(0) } DFields = "Return-Path:|X-Mailer:|Received:|Status:|" \ "Message-id:|Message-Id:|Message-ID:|References:|Reference:|"\ "X-Newsgroups:|X-Organization:|X-Internet:|Resent-Message-Id:"\ "X-Envelope-To:|X-Vms-To:|Distribution:|Errors-To:|Lines:" OFields = "Cc:|Bcc:|In-Reply-To:|Apparently-To:|To:|" \ "Really-From:|Newsgroups:|Organization:|Sender:|Posted-Date:|Phone-Number:|" \ "Address:|In-Real-Life:|Reply-To:|Source-Info:|Content-Type:|Content-Length:" \ RFields = "From|From:|Subject:|Date:" if ("d" in Options) OFields = OFields "|" DFields else MakeSet(DiscardFields,DFields,"|") DiscardFields["EditStatus:"] if ("m" in Options) OFields = OFields "|" RFields else MakeSet(ReqFields,RFields,"|") if ("u" in Options) NoUnReq = 1 else MakeSet(OptFields,OFields,"|") MakeSet(PrintFields,"From:|Subject:|To:|Cc:|Date:","|") if ((PAGER = ENVIRON["PAGER"]) == "") PAGER = "more" if ((EDITOR = ENVIRON["VISUAL"]) == "" && (EDITOR = ENVIRON["EDITOR"]) == "") EDITOR = "vi" MSep = "\1\1\1\1" for (ind = 1; !(ind in ARGV); ind++) ; InputFile = ARGV[ind] # Delete element so that we can read from tty as stdin (doesn't work!) delete ARGV[ind] ARGC = 0 if ("o" in Options) OutFile = Options["o"] else OutFile = InputFile ".ed" TmpName = InputFile ".tmp" "tput lines" | getline DisplayLines # Leave room for blank line & prompt line at end DisplayLines -= 2 for (NumMessages = 1; (Ret = \ GetMessage(InputFile,NumMessages,!("s" in Options))) == 1; NumMessages++) printf "." if (Ret == -1) { printf "Could not open input file \"%s\".\n",InputFile exit 1 } close(InputFile) NumMessages-- print "" for (i = 1; i <= NumMessages; i++) if (!(i in Status)) Status[i] = " " if ("n" in Options) SaveMessages("S",OutFile,0) else { MakeUncontrolTable() ProcMessages() } } # Save all messages in Messages that have status Type to file File. # If WriteStatus is true, add an "EditStatus:" field to the header # of messages with status Saved and Deleted, with value "S" or "D". # The number of messages saved is printed and returned. function SaveMessages(Type,File,WriteStatus, MessageNum,NumSaved) { NumSaved = 0 if (Readable(File)) { printf "File \"%s\" exists. Overwrite? ",File if (GetChar(1) !~ "[yY]") { print "Aborting save." return -1 } } for (MessageNum = 1; MessageNum <= NumMessages; MessageNum++) if (Status[MessageNum] ~ "[" Type "]") { print MSep > File SaveMessage(MessageNum,File,WriteStatus) print MSep > File NumSaved++ printf "." } print "" close(File) printf "%s message(s) saved to %s.\n",NumSaved,File return(NumSaved) } # Read next message from file File. # The messages are stored in Messages[Num,1..numlines]. # If LoadStatus is true and the message header has an EditStatus: field, # Status[Num] is set to its value. function GetMessage(File,Num,LoadStatus, Line,Ret,LineNum,Mod) { # A progress mark is printed every Mod lines. Mod = 100 # Read & discard message separator while ((Ret = (getline Line < File)) == 1 && Line == MSep) ; if (Ret != 1) return Ret Messages[Num,LineNum = 1] = Line while (getline Line < File == 1 && Line != MSep) { Messages[Num,++LineNum] = Line if (!(LineNum % Mod)) printf "%s\010",substr("|/-\\",LineNum / Mod % 4 + 1,1) } # Get rid of trailing blank lines while (LineNum && Messages[Num,LineNum] ~ "^[ \t]*$") delete Messages[Num,LineNum--] LastLine[Num] = LineNum Display[Num] = GetDisplay(Num,LoadStatus) return 1 } # Build a display from message number Num & return it. # Uses ReqFields, OptFields, and PrintFields function GetDisplay(Num,LoadStatus, Fields,Field,LastField,HeadErrors,HeaderPrint,Line,LineNum,NumLines, BodyPrint,BodyLines,DisplaySize,RealLines) { RealLines = NumLines = LastLine[Num] HeaderPrint = LastField = "" # Include space between header & body, and Lines: line DisplaySize = 2 for (LineNum = 1; LineNum <= NumLines && (Line = Messages[Num,LineNum]) != ""; LineNum++) { split(Line,Fields) if (Line ~ "^[ \t]") Field = LastField else Field = Fields[1] LastField = Field if (LoadStatus == 1 && Field == "EditStatus:") Status[Num] = Fields[2] if (Field in DiscardFields) { delete Messages[Num,LineNum] RealLines-- continue } if (Field in PrintFields) { HeaderPrint = HeaderPrint Line "\n" DisplaySize++ } if (Field in ReqFields) ReqFields[Field] = 1 else if (!NoUnReq && !(Field in OptFields) && Field == Fields[1]) { HeadErrors = HeadErrors "Unrecognized header field \"" Field "\".\n" DisplaySize++ } } HeaderLines[Num] = LineNum - 1 for (Field in ReqFields) if (ReqFields[Field]) ReqFields[Field] = 0; else { HeadErrors = HeadErrors "No \"" Field "\" field in header.\n" DisplaySize++ } HeaderPrint = HeadErrors HeaderPrint BodyPrint = "" if (NumLines == LineNum) BodyLines = 0 else { LineNum++ BodyLines = NumLines - LineNum for (; LineNum <= NumLines; LineNum++) { Line = Messages[Num,LineNum] gsub("\t"," ",Line) # Worst-case tab expansion DisplaySize += int(length(Line) / 80) + 1 if (DisplaySize <= DisplayLines) BodyPrint = BodyPrint Messages[Num,LineNum] "\n" else break } } Lines[Num] = RealLines return HeaderPrint "Lines: " max(BodyLines,0) "\n\n" BodyPrint } # Save message number MessageNum to file File. function SaveMessage(MessageNum,File,WriteStatus, LineNum,MLines,Line,MsgStatus,HLines) { Mod = 100 MLines = LastLine[MessageNum] HLines = HeaderLines[MessageNum] for (LineNum = 1; LineNum <= HLines; LineNum++) if (MessageNum SUBSEP LineNum in Messages) print Messages[MessageNum,LineNum] > File if (WriteStatus) { MsgStatus = Status[MessageNum] if (MsgStatus !~ "[ U]") printf "EditStatus: %s\n",MsgStatus > File } for (; LineNum <= MLines; LineNum++) { print Messages[MessageNum,LineNum] > File if (!(LineNum % Mod)) printf "%s\010",substr("|/-\\",LineNum / Mod % 4 + 1,1) } } # Return a string listing the options given in Words function OptionList(List,Words, S,Len,i,Opt,OptLen,LineLen) { Len = length(List) S = Words[substr(List,1,1)] LineLen = length(S) for (i = 2; i <= Len; i++) { Opt = substr(List,i,1) OptLen = length(Words[Opt]) if (LineLen + OptLen > 78) { S = S "\n" Words[Opt] LineLen = OptLen } else { S = S " " Words[Opt] LineLen += OptLen + 1 } } return S } # Return a string listing the options given in Words and Help function HelpList(List,Words,Help, S,Len,i,c) { Len = length(List) c = substr(List,1,1) S = sprintf("%-11s%s",Words[c],Help[c]) for (i = 2; i <= Len; i++) { c = substr(List,i,1) S = S sprintf("\n%-11s%s",Words[c],Help[c]) } return S } # Return the value of the Field field of message number Num, # or a null string if it does not exist function FindField(Num,Field, Line) { Line = FindHeaderLine(Num,Field "[ \t]") return substr(Line,length(Field) + 1) } # Return a line giving the number, status, and subject of message number Num function MessageLine(Num) { return sprintf("%3d %s %s",Num,Status[Num],FindField(Num,"Subject:")) } # Return a line giving statistics about the messages function MessageStats( i,NumStat) { for (i = 1; i <= NumMessages; i++) NumStat[Status[i]]++ return sprintf("%d messages: %d saved, %d deleted, %d unmarked.", \ NumMessages,NumStat["S"],NumStat["D"], \ NumMessages - NumStat["S"] - NumStat["D"]) } # Return the first line in the header of message Num that matches Pattern function FindHeaderLine(Num,Pattern, LineNum,Line,Lines) { Lines = HeaderLines[Num] for (LineNum = 1; LineNum <= Lines; LineNum++) if ((Num SUBSEP LineNum in Messages) && \ ((Line = Messages[Num,LineNum]) ~ Pattern)) return Line return "" } # Global variables for static storage: OldFile function MessageCmd(Resp,MessageNum,Param, i,LineNum,Sign) { CloseFile = "" if (Resp ~ "[sud]") Status[MessageNum] = toupper(Resp) else if (Resp == "l") printf "%3d %s %s\n",MessageNum,Status[MessageNum], \ FindField(MessageNum,"Subject:") else if (Resp == "f") { if (Param == "") Param = OldFile if (Param == "") print "Null filename." else { print MSep >> Param SaveMessage(MessageNum,Param,0) print MSep > Param CloseFile = Param } } else if (Resp == "p") { for (LineNum = 1; LineNum <= LastLine[MessageNum]; LineNum++) if (MessageNum SUBSEP LineNum in Messages) print Messages[MessageNum,LineNum] | PAGER CloseFile = PAGER } else if (Resp ~ "[ve]") { SaveMessage(MessageNum,TmpName,0) close(TmpName) system(EDITOR " " TmpName) if (GetMessage(TmpName,MessageNum,0) == -1) print ("Could not read temp file \"%s\".\n",TmpName) close(TmpName) system("rm " TmpName) } else if (Resp == "z") { if (Param == "") Param = "+0" if (Param ~ "[-+]") { Sign = (substr(Param,1,1) 1) + 0 Param = substr(Param,2) } else Sign = 0 if (sign(Lines[MessageNum] - Param) == Sign) printf "%4d %s\n",Lines[MessageNum],MessageLine(MessageNum) } return CloseFile } function ProcMessages( Resp,MessageNum,ValidOpt,NewNum,Param, OldPat,Words,Help,AllOpts,i,Error,PrevNum,OldNum,Direction, ListTo,ListFrom,Prompt,OldFile) { AllOpts = "k,s,d,q,w,c,a,h,p,e,r,b,u,l,f,-,/,?,#,z" InitArr(Words,AllOpts, \ "sKip,Save,Delete,Quit,Write,Checkpoint,Abort,Help,Page,Edit,Redraw," \ "Back,Unmark,List,File,-previous,/search,?search,#info,siZe",",") gsub(",","\n",AllOpts) InitArr(Help,AllOpts, \ "Skip over message without marking it.\n" \ "Mark message to be saved.\n" \ "Mark message to be deleted.\n" \ "Write messages marked to be saved to the output mailbox and quit.\n" \ "Save all messages and markings to a new mailbox.\n" \ "Write unmarked & save-marked messages and markings to a new mailbox.\n" \ "Exit from this program without saving anything.\n" \ "Print this help.\n" \ "Pipe entire message into pager ($PAGER or \"more\")\n" \ "Invoke editor $VISUAL, $EDITOR, or \"vi\" on message (also: v).\n" \ "Print the display excerpt of the message again (also: ^L, ^R).\n" \ "Go back one message.\n" \ "Remove any marking of the message.\n" \ "List status & subjects of messages (also: =).\n" \ "Save message to a file (appending if it exists) in mailbox format.\n" \ "Go to the previous message displayed.\n" \ "Search forward for messages with a pattern in the header.\n" \ "Search backward for messages with a pattern in the header.\n" \ "Print statistics about messages.\n" \ "Find messages by number of lines (enter lines, +lines, or -lines)." \ ,"\n") gsub("\n","",AllOpts) AllOpts = AllOpts "clear" | getline ClearSeq MessageNum = 1 PrevNum = 0 InitArr(OptTrans,"v,=,\014,\022","e,l,r,r",",") InitArr(Prompts,"c,w,f","Mailbox name: ,Mailbox name: ,File name: ",",") ParamOpts = "1-9/?cwfz" while (1) { if (PrevNum != MessageNum) { if (MessageNum > NumMessages) ValidOpt = "hqwcabl-?#" else { ValidOpt = AllOpts print ClearSeq Display[MessageNum] } PrevNum = MessageNum } if (MessageNum > NumMessages) Prompt = sprintf("No more messages: [%s] %s",ValidOpt,Error) else Prompt = sprintf("%s %3d [%s] %s",Status[MessageNum], \ MessageNum,ValidOpt,Error) Error = "" Resp = GetCommand(ValidOpt "123456789",Prompt,OptTrans, \ ParamOpts,Prompts,OptionList(ValidOpt,Words)) Param = substr(Resp,2) Resp = substr(Resp,1,1) if (Resp ~ "[1-9]") { NewNum = Resp Param if (NewNum !~ "[0-9]+") { print "Bad number." continue } NewNum += 0 if (NewNum > NumMessages) print "Not that many messages." else MessageNum = NewNum } else if (Resp ~ "[k ]") MessageNum++ else if (Resp == "a") { printf "Abort without saving... are you sure? [yn] " Resp = tolower(GetChar(1)) if (Resp == "y") return } else if (Resp == "q") { for (i = 1; i <= NumMessages && Status[i] ~ "[SD]"; i++) ; if (i > NumMessages) { if (SaveMessages("S",OutFile,0) != -1) return } else { Error = "(Unmarked Message) " MessageNum = i } } else if (Resp ~ "[cw]") { if (Param == "") Param = OldFile if (Param == "") print "Null filename." else { if (Resp == "c") Stat = " SU" else Stat = " SUD" SaveMessages(Stat,Param,1) } } else if (Resp == "h") print OptionList(ValidOpt,Words) "\n" HelpList(ValidOpt,Words,Help) else if (Resp == "r") print ClearSeq Display[MessageNum] else if (Resp == "b") MessageNum = max(1,MessageNum - 1) else if (Resp == "-") MessageNum = PrevNum else if (Resp == "#") print MessageStats() else if (Resp == "?" || Resp == "/") { if (Param == "") Param = OldPat if (Param == "") { print "Null pattern." continue } OldPat = Param Direction = (Resp == "/") * 2 - 1 Count = 0 for (i = MessageNum; 1 <= i && i <= NumMessages && \ Count < DisplayLines; i += Direction) if (FindHeaderLine(i,Param) != "") { print MessageLine(i) Count++ } } else if (Resp ~ "[sud]") MessageCmd(Resp,MessageNum++,Param) else if (Resp == "l") { ListTo = min(NumMessages,MessageNum + DisplayLines - 1) ListFrom = max(1,ListTo - DisplayLines + 1) for (i = ListFrom; i <= ListTo; i++) MessageCmd(Resp,i,"") } else if (Resp ~ "[vefp]") { if ((CloseFile = MessageCmd(Resp,MessageNum,Param)) != "") close(CloseFile) } else if (Resp == "z") { if (Param !~ "[-+1-9][0-9]+") { print "Bad range." continue } for (i = 1; i <= NumMessages; i++) MessageCmd(Resp,i,Param) } else print "uh oh..." } } # Library Routines # MakeSet: make a set from a list. # An index with the name of each element of the list # is created in the given array. # Input variables: # Elements is a string containing the list of elements. # Sep is the character that separates the elements of the list. # Output variables: # Set is the array. function MakeSet(Set,Elements,Sep, Num,Names) { Num = split(Elements,Names,Sep) for (; Num; Num--) Set[Names[Num]]; } function min(a,b) { if (a < b) return a else return b } function max(a,b) { if (a > b) return a else return b } # InitArr: Initialize an array with values. # Ind and Vals are separated into lists on Sep. # For each item in Ind, an index with that name is created in Arr[], # and the value with the same position in Vals is stored in it. # Global variables: none. function InitArr(Arr,Ind,Vals,sep, numind,indnames,values) { split(Ind,indnames,sep) split(Vals,values,sep) for (numind in indnames) Arr[indnames[numind]] = values[numind] } # ProcArgs.awk john h. dubois iii 92/02/29 # optlist is a string which contains all of the possible command line options. # If a character is followed by a colon, # it indicates that that option takes an argument. # Strings in argv[] which begin with "-" or "+" are taken to be # strings of options, except that a string which consists solely of "-" or # "+" is not taken to be an option string (it is not acted on). # If an option takes an argument, the argument may either immedately # follow it or be given separately. # If an option that does not take an argument is given, # an index with its name is created in options and its value is set to "1". # If an option that does take an argument is given, # an index with its name is created in options and its value # is set to the value of the argument given for it. # Options and their arguments are deleted from argv. # Note that this means that there may be gaps # left in the indices of argv[]. # argv[0] is not examined. # An argument of "--" or "++" stops the scanning of argv. # The number of arguments left in argc is returned. # If an error occurs, # the string OptErr is set to an error message and -1 is returned. function ProcArgs(argc,argv,optlist,options, ArgNum,ArgsLeft,Arg,ArgLen,ArgInd,Option,Pos) { # ArgNum is the index of the argument being processed. # ArgsLeft is the number of arguments left in argv. # Arg is the argument being processed. # ArgLen is the length of the argument being processed. # ArgInd is the position of the character in Arg being processed. # Option is the character in Arg being processed. # Pos is the position in optlist of the option being processed. ArgsLeft = argc for (ArgNum = 1; ArgNum < argc; ArgNum++) { Arg = argv[ArgNum] if (Arg ~ "^[-+]") { if ((Arg == "-") || (Arg == "+")) continue delete argv[ArgNum] ArgsLeft-- if ((Arg == "--") || (Arg == "++")) break ArgLen = length(Arg) for (ArgInd = 2; ArgInd <= ArgLen; ArgInd++) { Option = substr(Arg,ArgInd,1) Pos = index(optlist,Option) if (!Pos) { OptErr = "Invalid option: -" Option return -1 } if (substr(optlist,Pos + 1,1) == ":") { if (ArgInd < ArgLen) { options[Option] = substr(Arg,ArgInd + 1) ArgInd = ArgLen } else { if (ArgNum < (argc - 1)) { options[Option] = argv[++ArgNum] delete argv[ArgNum] ArgsLeft-- } else { OptErr = "Option -" Option " requires an argument." return -1 } } } else options[Option] = 1 } } } return ArgsLeft } # Uncontrol(S): Convert control characters in S to symbolic form. # Characters in S with values < 32 are converted to the form ^X. # The resulting string is returned. # Global variables: the array UncTable must be initialized to a # lookup table translating characters 0..31 to symbolic values # before using this function. function Uncontrol(S, c,i,len,Output) { len = length(S) Output = "" for (i = 1; i <= len; i++) { c = substr(S,i,1) if (c in UncTable) Output = Output UncTable[c] else Output = Output c } return Output } # MakeUncontrolTable: Make a table for use by Uncontrol(). # Global variables: # UncTable[] is made into a character -> symbolic character lookup table. function MakeUncontrolTable( i) { for (i = 0; i < 32; i++) UncTable[sprintf("%c",i)] = "^" sprintf("%c",i + 64) } function sign(value) { if (value > 0) return 1 else if (value < 0) return -1 else return 0 } # Read & return a line. # Needed because awk won't read stdin if any args were given. # If the read fails, an error message is printed and Err is returned. function TTYLine(Err, Ret,ReadLine,Line) { # Must read from /dev/tty due to gawk bug ReadLine = "read line < /dev/tty && echo \"$line\"" Ret = (ReadLine | getline Line) close(ReadLine) if (Ret == 1) return Line else { print "Couldn't read from tty!" return Err } } # Get a character from stdin in non-canonical mode & return it. # The character is echoed. # If PrintNewline is 1, a newline is printed after the character is read. function GetChar(PrintNewline, ChProc,Resp) { # Must read from /dev/tty to work around gawk bug ChProc = "getch e < /dev/tty" ChProc | getline Resp close(ChProc) if (PrintNewline) print "" return Resp } # Returns 0 if FileName can't be read, else nonzero. function Readable(FileName, foo,Ret) { Ret = getline foo < FileName close(FileName) return (Ret != -1) } # Wait for a key to be pressed & return it. # Prompt is printed to prompt for a key. # Options is a string containing all of the valid commands. # KeyMap is an array mapping command synonyms to characters in Options. # Help is a string to print if an invalid key is pressed. # If space is pressed, it is mapped to the first character in Options. # If a character in the string ParamOpts is pressed, # a string is read and appended to the key in the return value. # If the character is an index of the array Prompts, # the value in Prompts for the character is printed before reading the string. # If the key pressed is not in ParamOpts, # a newline is printed before returning. function GetCommand(Options,Prompt,KeyMap,ParamOpts,Prompts,Help, Done,Key,Cmd,Param) { Done = 0 while (!Done) { printf "%s",Prompt Key = GetChar() if (Key == " ") Cmd = Key = substr(Options,1,1) else Cmd = tolower(Key) Key = Uncontrol(Key) if (Cmd in KeyMap) Cmd = KeyMap[Cmd] if (index(Options,Cmd)) { if (index(ParamOpts,Cmd)) { if (Cmd in Prompts) printf "\n%s",Prompts[Cmd] if ((Param = TTYLine("\200")) != "\200") return Cmd Param } print "" return Cmd } else printf "\nInvalid command '%s'\n%s\n",Key,Help } }