############################################################################ # # File: post.icn # # Subject: Program to post news # # Author: Ronald Florence # # Date: August 14, 1996 # ############################################################################ # # This file is in the public domain. # ############################################################################ # # Version: 1.5 # ############################################################################ # # This program posts a news article to Usenet. Given an optional # argument of the name of a file containing a news article, or an # argument of "-" and a news article via stdin, post creates a # follow-up article, with an attribution and quoted text. The # newsgroups, subject, distribution, follow-up, and quote-prefix can # optionally be specified on the command line. # # usage: post [options] [article | -] # -n newsgroups # -s subject # -d distribution # -f followup-to # -p quote-prefix (default ` > ') # # See the site & system configuration options below. On systems # posting via inews, post validates newsgroups and distributions in # the `active' and `distributions' files in the news library directory. # ############################################################################ # # Bugs: Newsgroup validation assumes the `active' file is sorted. # Non-UNIX sites need hardcoded system information. # ############################################################################ # # Links: options # ############################################################################ link options global mode, sysname, domain, tz, tmpfile, opts, console, newslib, org procedure main(arg) local usage, smarthost, editor, default_distribution, generic_from local tmpdir, logname, fullname, sigfile, article, inf, edstr, outf, tmp2 usage := ["usage: post [options] [article]", "\t-n newsgroups", "\t-s subject", "\t-d distribution", "\t-f followup-to", "\t-p quote-prefix (default ` > ')", "\t- read article from stdin"] # Site configuration. Mode can be # "local" (post via inews), # "uux" (post via rnews to an upstream host), # "mail" (post via mail to an upstream host). # For either uux or mail mode, # smarthost := the uucp nodename of the upstream news feed. # Use generic_from to force a generic address instead # of the hostname provided by system commands. mode := "local" smarthost := "" editor := "vi" domain := ".UUCP" default_distribution := "world" generic_from := &null # For UNIX, the rest of the configuration is automatic. if find("UNIX", &features) then { console := "/dev/tty" newslib := "/usr/lib/news/" tz := "unix" tmpdir := "/tmp/" logname := pipe("logname") sysname := trim(pipe("hostname", "uname -n", "uuname -l")) # BSD passwd: `:fullname[,...]:' # SysV passwd: `-fullname(' \logname & every lookup("/etc/passwd") ? { =(logname) & { every tab(upto(':')+1) \4 fullname := (tab(upto('-')+1), tab(upto('(:'))) | tab(upto(',:')) break } } sigfile := getenv("HOME") || "/.signature" } # For non-UNIX systems, we need hard coded configuration: # console := the system's name for the user's terminal. # libdir := the directory for news configuration files, like # an `organization' file. # tmpdir := optional directory for temporary files; terminated # with the appropriate path separator: `/' or `\\'. # logname := user's login name. # tz := local time zone (e.g., EST). # fullname := user's full name. # sigfile := full path of file with user's email signature. else { console := "CON" newslib := "" tmpdir := "" logname := &null tz := &null fullname := &null sigfile := &null sysname := getenv("HOST") | &host } # End of user configuration. (\logname & \sysname & \tz & (mode == "local" | *smarthost > 0)) | stop("post: missing system information") opts := options(arg, "n:s:d:f:p:h?") \opts["h"] | \opts["?"] | arg[1] == "?" & { every write(!usage) exit(-1) } org := getenv("ORGANIZATION") | lookup(newslib || "organization") article := open(tmpfile := tempname(tmpdir), "w") | stop("post: cannot write temp file") write(article, "Path: ", sysname, "!", logname) writes(article, "From: ", logname, "@", \generic_from | sysname, domain) \fullname & writes(article, " (", fullname, ")") write(article) # For a follow-up article, reply_headers() does the work. if \arg[1] then { inf := (arg[1] == "-" & &input) | open(arg[1]) | (remove(tmpfile) & stop("post: cannot read " || arg[1])) reply_headers(inf, article) every write(article, \opts["p"] | " > ", !inf) close(inf) } # Query if newsgroups, subject, and distribution have # not been specified on the command line. else { write(article, "Newsgroups: ", validate(\opts["n"] | query("Newsgroups: "), "active")) write(article, "Subject: ", \opts["s"] | query("Subject: ")) write(article, "Distribution: ", validate(\opts["d"] | query("Distribution: ", default_distribution), "distributions")) every write(article, req_headers()) write(article, "\n") } close(article) edstr := (getenv("EDITOR") | editor) || " " || tmpfile || " < " || console system(edstr) upto('nN', query("Are you sure you want to post this to Usenet y/n? ")) & { if upto('yY', query("Save your draft article y/n? ")) then stop("Your article is saved in ", tmpfile) else { remove(tmpfile) stop("Posting aborted.") } } # For inews, we supply the headers, inews supplies the .signature. if mode == "local" then mode := newslib || "inews -h" else { \sigfile & { article := open(tmpfile, "a") write(article, "--") every write(article, lookup(sigfile)) } # To post via sendnews (mail), we prefix lines with 'N'. # For rnews, don't force an immediate poll. case mode of { "mail": { mode ||:= " " || smarthost || "!rnews" outf := open(tmp2 := tempname(tmpdir), "w") every write(outf, "N", lookup(tmpfile)) remove(tmpfile) rename(tmp2, tmpfile) } "uux": mode ||:= " - -r " || smarthost || "!rnews" } } mode ||:= " < " || tmpfile (system(mode) = 0) & write("Article posted!") remove(tmpfile) end # To parse the original article, we use case-insensitive # matches on the headers. The Reply-to and Followup-To # headers usually appear later than From and Newsgroups, so # they take precedence. By usenet convention, we query # the user if Followup-To on the original is `poster'. procedure reply_headers(infile, art) local fullname, address, quoter, date, id, subject, distribution local group, refs every !infile ? { tab(match("from: " | "reply-to: ", map(&subject))) & { if find("<") then { fullname := (trim(tab(upto('<'))) ~== "") address := (move(1), tab(find(">"))) } else { address := trim(tab(upto('(') | 0)) fullname := (move(1), tab(find(")"))) } quoter := (\fullname | address) } tab(match("date: ", map(&subject))) & date := tab(0) tab(match("message-id: ", map(&subject))) & id := tab(0) tab(match("subject: ", map(&subject))) & subject := tab(0) tab(match("distribution: ", map(&subject))) & distribution := tab(0) tab(match("newsgroups: " | "followup-to: ", map(&subject))) & group := tab(0) tab(match("references: ", map(&subject))) & refs := tab(0) (\quoter & *&subject = 0) & { find("poster", group) & { write(quoter, " has requested followups by email.") upto('yY', query("Do you want to abort this posting y/n? ")) & { remove(tmpfile) stop("Posting aborted.") } group := &null } write(art, "Newsgroups: ", \group | validate(\opts["n"] | query("Newsgroups: "), "active")) write(art, "Subject: ", \opts["s"] | \subject | query("Subject: ")) \distribution | distribution := validate(\opts["d"], "distributions") & write(art, "Distribution: ", distribution) write(art, "References: ", (\refs ||:= " ") | "", id) every write(art, req_headers()) write(art, "In-reply-to: ", quoter, "'s message of ", date) write(art, "\nIn ", id, ", ", quoter, " writes:\n") return } } end # We need a unique message-id, and a date in RFC822 format. # Easy with UNIX systems that support `date -u'; with the # others, we leave the local timezone. The first inews site # will correct it. procedure req_headers() local uniq, date, month, day, time, zone, year uniq := "<" &date || &clock ? while tab(upto(&digits)) do uniq ||:= tab(many(&digits)) uniq ||:= "@" || sysname || domain || ">" if tz == "unix" then { date := pipe("date -u", "date") date ? { month := (tab(find(" ") + 1), tab(many(&letters))) day := (tab(upto(&digits)), tab(many(&digits))) time := (tab(upto(&digits++':')), tab(many(&digits++':'))) zone := (tab(upto(&ucase)), tab(many(&ucase))) year := (tab(upto(&digits)+ 2), tab(0)) } date := day || " " || month || " " || year || " " || time || " " || zone } else { &dateline ? { month := left((tab(find(" ")+1), tab(many(&letters))), 3) || " " date := (tab(upto(&digits)), tab(many(&digits))) || " " || month date ||:= (tab(upto(&digits)), right(tab(many(&digits)), 2)) } date ||:= " " || &clock || " " || tz } mode ~== "local" & suspend "Message-ID: " || uniq suspend "Date: " || date \org & suspend "Organization: " || org \opts["f"] & return "Followup-To: " || ((opts["f"] == "poster") | validate(opts["f"], "active")) end # Richard Goerwitz's generator. procedure tempname(dir) local temp_name every temp_name := dir || "article." || right(1 to 999,3,"0") do { close(open(temp_name)) & next suspend \temp_name } end # On systems with pipes, pipe() will read from the first # successful command of the list given as arguments. procedure pipe(cmd[]) local inf, got initial find("pipes" | "compiled", &features) | stop("No pipes.") while inf := open("(" || pop(cmd) || ") 2>&1", "pr") do { got := [] every put(got, !inf) close(inf) = 0 & { suspend !got break } } end # The dirty work of reading from a file. procedure lookup(what) local inf inf := open(what, "r") | fail suspend !inf close(inf) end # Query opens stdin because the system call to the editor # redirects input. The optional parameter is a default # response if the user answers with . procedure query(prompt, def) local ans static stdin initial stdin := open(console) writes(prompt) ans := read(stdin) return (*ans = 0 & \def) | ans end # A quick and dirty kludge. Validate() builds a sorted list. # When an element is found, it is popped and the search moves # to the next item. The procedure assumes the file is also # sorted. procedure validate(what, where) local valid, stuff, sf, a mode ~== "local" & return what valid := &letters ++ '.-' ++ &digits stuff := [] what ? while tab(upto(valid)) do put(stuff,tab(many(valid))) sf := open(newslib || where) | { remove(tmpfile) stop("post: cannot open ", newslib || where) } stuff := sort(stuff) a := pop(stuff) every !sf ? match(a) & (a := pop(stuff)) | return what remove(tmpfile) stop("`", a, "' is not in ", newslib || where) end