home *** CD-ROM | disk | FTP | other *** search
/ ftp.cs.arizona.edu / ftp.cs.arizona.edu.tar / ftp.cs.arizona.edu / icon / historic / v941.tgz / icon.v941src.tar / icon.v941src / ipl / progs / post.icn < prev    next >
Text File  |  2000-07-29  |  12KB  |  367 lines

  1. ############################################################################
  2. #
  3. #    File:     post.icn
  4. #
  5. #    Subject:  Program to post news
  6. #
  7. #    Author:   Ronald Florence
  8. #
  9. #    Date:     August 14, 1996
  10. #
  11. ############################################################################
  12. #
  13. #   This file is in the public domain.
  14. #
  15. ############################################################################
  16. #
  17. #    Version:  1.5
  18. #
  19. ############################################################################
  20. #
  21. #  This program posts a news article to Usenet.  Given an optional
  22. #  argument of the name of a file containing a news article, or an
  23. #  argument of "-" and a news article via stdin, post creates a
  24. #  follow-up article, with an attribution and quoted text.  The
  25. #  newsgroups, subject, distribution, follow-up, and quote-prefix can
  26. #  optionally be specified on the command line.
  27. #
  28. #      usage: post [options] [article | -]
  29. #        -n newsgroups
  30. #        -s subject
  31. #        -d distribution
  32. #        -f followup-to
  33. #        -p quote-prefix (default ` > ')
  34. #
  35. #  See the site & system configuration options below.  On systems
  36. #  posting via inews, post validates newsgroups and distributions in
  37. #  the `active' and `distributions' files in the news library directory.
  38. #
  39. ############################################################################
  40. #
  41. #  Bugs: Newsgroup validation assumes the `active' file is sorted.
  42. #        Non-UNIX sites need hardcoded system information.
  43. #
  44. ############################################################################
  45. #
  46. #  Links: options
  47. #
  48. ############################################################################
  49.  
  50. link options
  51.  
  52. global mode, sysname, domain, tz, tmpfile, opts, console, newslib, org
  53.  
  54. procedure main(arg)
  55.   local usage, smarthost, editor, default_distribution, generic_from
  56.   local tmpdir, logname, fullname, sigfile, article, inf, edstr, outf, tmp2
  57.  
  58.   usage := ["usage: post [options] [article]",
  59.             "\t-n newsgroups",
  60.             "\t-s subject",
  61.             "\t-d distribution",
  62.             "\t-f followup-to",
  63.             "\t-p quote-prefix (default ` > ')",
  64.             "\t-  read article from stdin"]
  65.  
  66.     # Site configuration.   Mode can be
  67.     #   "local" (post via inews),
  68.     #   "uux" (post via rnews to an upstream host),
  69.     #   "mail" (post via mail to an upstream host).
  70.     # For either uux or mail mode,
  71.     #   smarthost := the uucp nodename of the upstream news feed.
  72.       # Use generic_from to force a generic address instead
  73.         #   of the hostname provided by system commands.
  74.  
  75.   mode := "local"        
  76.   smarthost := ""
  77.   editor := "vi"
  78.   domain := ".UUCP"
  79.   default_distribution := "world" 
  80.   generic_from := &null
  81.  
  82.     # For UNIX, the rest of the configuration is automatic.
  83.  
  84.   if find("UNIX", &features) then {
  85.     console := "/dev/tty"
  86.     newslib := "/usr/lib/news/"
  87.     tz := "unix"
  88.     tmpdir := "/tmp/"
  89.     logname := pipe("logname")
  90.     sysname := trim(pipe("hostname", "uname -n", "uuname -l"))
  91.     # BSD  passwd: `:fullname[,...]:'
  92.     # SysV passwd: `-fullname(' 
  93.     \logname & every lookup("/etc/passwd") ? {
  94.       =(logname) & {
  95.     every tab(upto(':')+1) \4 
  96.     fullname := (tab(upto('-')+1), tab(upto('(:'))) | tab(upto(',:'))
  97.     break
  98.       }    
  99.     }
  100.     sigfile := getenv("HOME") || "/.signature"
  101.   }
  102.  
  103.     # For non-UNIX systems, we need hard coded configuration:
  104.     # console := the system's name for the user's terminal.
  105.     # libdir := the directory for news configuration files, like 
  106.     #           an `organization' file.
  107.     # tmpdir := optional directory for temporary files; terminated 
  108.     #        with the appropriate path separator: `/' or `\\'.
  109.     # logname := user's login name.
  110.     # tz := local time zone (e.g., EST).
  111.         # fullname := user's full name.
  112.         # sigfile := full path of file with user's email signature.
  113.  
  114.   else {
  115.     console := "CON"
  116.     newslib := ""
  117.     tmpdir := ""
  118.     logname := &null
  119.     tz := &null
  120.     fullname := &null 
  121.     sigfile := &null
  122.     sysname := getenv("HOST") | &host
  123.   }
  124.  
  125.     # End of user configuration.
  126.  
  127.   (\logname & \sysname & \tz & (mode == "local" | *smarthost > 0)) |
  128.     stop("post: missing system information")
  129.   opts := options(arg, "n:s:d:f:p:h?") 
  130.   \opts["h"] | \opts["?"] | arg[1] == "?" & { 
  131.     every write(!usage)
  132.     exit(-1) 
  133.   }
  134.   org := getenv("ORGANIZATION") | lookup(newslib || "organization")
  135.   article := open(tmpfile := tempname(tmpdir), "w") | 
  136.     stop("post: cannot write temp file")
  137.   write(article, "Path: ",  sysname, "!", logname)
  138.   writes(article, "From: ", logname, "@", \generic_from | sysname, domain)
  139.   \fullname & writes(article, " (", fullname, ")")
  140.   write(article)
  141.  
  142.     # For a follow-up article, reply_headers() does the work. 
  143.  
  144.   if \arg[1] then {
  145.     inf := (arg[1] == "-" & &input) | 
  146.       open(arg[1]) | (remove(tmpfile) & stop("post: cannot read " || arg[1]))
  147.     reply_headers(inf, article)
  148.     every write(article, \opts["p"] | " > ", !inf)
  149.     close(inf)
  150.   }
  151.  
  152.     # Query if newsgroups, subject, and distribution have
  153.         # not been specified on the command line.
  154.  
  155.   else {
  156.     write(article, "Newsgroups: ", 
  157.       validate(\opts["n"] | query("Newsgroups: "), "active"))
  158.     write(article, "Subject: ", \opts["s"] | query("Subject: "))
  159.     write(article, "Distribution: ", 
  160.       validate(\opts["d"] | query("Distribution: ", default_distribution), 
  161.            "distributions"))
  162.     every write(article, req_headers())
  163.     write(article, "\n")
  164.   }
  165.   close(article)
  166.   edstr := (getenv("EDITOR") | editor) || " " || tmpfile || " < " || console
  167.   system(edstr)
  168.   upto('nN', query("Are you sure you want to post this to Usenet y/n? ")) & {
  169.     if upto('yY', query("Save your draft article y/n? ")) then 
  170.       stop("Your article is saved in ", tmpfile)
  171.     else {
  172.       remove(tmpfile)
  173.       stop("Posting aborted.") 
  174.     }
  175.   }
  176.     # For inews, we supply the headers, inews supplies the .signature.
  177.  
  178.   if mode == "local" then mode := newslib || "inews -h"
  179.   else {
  180.     \sigfile & {
  181.       article := open(tmpfile, "a")
  182.       write(article, "--")  
  183.       every write(article, lookup(sigfile))
  184.     }
  185.     # To post via sendnews (mail), we prefix lines with 'N'.
  186.     # For rnews, don't force an immediate poll.
  187.  
  188.     case mode of {
  189.       "mail": {
  190.              mode ||:= " " || smarthost || "!rnews" 
  191.          outf := open(tmp2 := tempname(tmpdir), "w")
  192.          every write(outf, "N", lookup(tmpfile))
  193.          remove(tmpfile)
  194.          rename(tmp2, tmpfile)
  195.        }
  196.       "uux": mode ||:= " - -r " || smarthost || "!rnews"
  197.     }
  198.   }
  199.   mode ||:= " < " || tmpfile
  200.   (system(mode) = 0) & write("Article posted!")
  201.   remove(tmpfile)
  202. end
  203.  
  204.     # To parse the original article, we use case-insensitive
  205.     # matches on the headers.  The Reply-to and Followup-To
  206.     # headers usually appear later than From and Newsgroups, so
  207.     # they take precedence.  By usenet convention, we query
  208.     # the user if Followup-To on the original is `poster'.
  209.  
  210. procedure reply_headers(infile, art)
  211.   local fullname, address, quoter, date, id, subject, distribution
  212.   local group, refs
  213.  
  214.   every !infile ? {
  215.     tab(match("from: " | "reply-to: ", map(&subject))) & {
  216.       if find("<") then {
  217.     fullname := (trim(tab(upto('<'))) ~== "")
  218.     address := (move(1), tab(find(">")))
  219.       }
  220.       else {
  221.     address := trim(tab(upto('(') | 0))
  222.     fullname := (move(1), tab(find(")")))
  223.       }
  224.       quoter := (\fullname | address)
  225.     }
  226.     tab(match("date: ", map(&subject))) & date := tab(0)
  227.     tab(match("message-id: ", map(&subject))) & id := tab(0)
  228.     tab(match("subject: ", map(&subject))) & subject := tab(0)
  229.     tab(match("distribution: ", map(&subject))) & distribution := tab(0)
  230.     tab(match("newsgroups: " | "followup-to: ", map(&subject))) & 
  231.       group := tab(0)
  232.     tab(match("references: ", map(&subject))) & refs := tab(0)
  233.     (\quoter & *&subject = 0) & {
  234.       find("poster", group) & {
  235.     write(quoter, " has requested followups by email.")
  236.     upto('yY', query("Do you want to abort this posting y/n? ")) & {
  237.       remove(tmpfile)
  238.       stop("Posting aborted.")
  239.     }
  240.     group := &null
  241.       }
  242.       write(art, "Newsgroups: ", \group | 
  243.     validate(\opts["n"] | query("Newsgroups: "), "active"))
  244.       write(art, "Subject: ", \opts["s"] | \subject | query("Subject: "))
  245.       \distribution | distribution := validate(\opts["d"], "distributions") &
  246.         write(art, "Distribution: ", distribution)
  247.       write(art, "References: ", (\refs ||:= " ") | "", id)
  248.       every write(art, req_headers())
  249.       write(art, "In-reply-to: ", quoter, "'s message of ", date)
  250.       write(art, "\nIn ", id, ", ", quoter, " writes:\n")
  251.       return 
  252.     }
  253.   }
  254. end
  255.  
  256.     # We need a unique message-id, and a date in RFC822 format.
  257.     # Easy with UNIX systems that support `date -u'; with the
  258.     # others, we leave the local timezone.  The first inews site
  259.     # will correct it.
  260.  
  261. procedure req_headers()
  262.   local uniq, date, month, day, time, zone, year
  263.  
  264.   uniq := "<"
  265.   &date || &clock ? while tab(upto(&digits)) do uniq ||:= tab(many(&digits))
  266.   uniq ||:= "@" || sysname || domain || ">"
  267.   if tz == "unix" then {
  268.     date := pipe("date -u", "date")
  269.     date ? {
  270.      month := (tab(find(" ") + 1), tab(many(&letters)))
  271.      day := (tab(upto(&digits)), tab(many(&digits)))
  272.      time := (tab(upto(&digits++':')), tab(many(&digits++':')))
  273.      zone := (tab(upto(&ucase)), tab(many(&ucase)))
  274.      year := (tab(upto(&digits)+ 2), tab(0))
  275.     }
  276.     date := day || " " || month || " " || year ||  " " || time || " " || zone
  277.   }
  278.   else {
  279.     &dateline ? {
  280.       month := left((tab(find(" ")+1), tab(many(&letters))), 3) || " "
  281.       date := (tab(upto(&digits)), tab(many(&digits))) || " " || month
  282.       date ||:= (tab(upto(&digits)), right(tab(many(&digits)), 2))
  283.     }
  284.     date ||:= " " || &clock || " " || tz
  285.   }
  286.   mode ~== "local" & suspend "Message-ID: " || uniq
  287.   suspend "Date: " || date 
  288.   \org & suspend "Organization: " || org
  289.   \opts["f"] & return "Followup-To: " || ((opts["f"] == "poster") | 
  290.     validate(opts["f"], "active"))
  291. end
  292.  
  293.     # Richard Goerwitz's generator.
  294.  
  295. procedure tempname(dir)
  296.   local temp_name
  297.  
  298.   every temp_name := dir || "article." || right(1 to 999,3,"0") do {
  299.     close(open(temp_name)) & next
  300.     suspend \temp_name
  301.   }
  302. end
  303.  
  304.     # On systems with pipes, pipe() will read from the first
  305.     # successful command of the list given as arguments.
  306.  
  307. procedure pipe(cmd[])
  308.   local inf, got
  309.  
  310.   initial find("pipes" | "compiled", &features) | stop("No pipes.")
  311.   while inf := open("(" || pop(cmd) || ") 2>&1", "pr") do {
  312.     got := []
  313.     every put(got, !inf)
  314.     close(inf) = 0 & {
  315.       suspend !got 
  316.       break
  317.     }
  318.   }
  319. end
  320.  
  321.     # The dirty work of reading from a file.
  322.  
  323. procedure lookup(what)
  324.   local inf
  325.  
  326.   inf := open(what, "r") | fail
  327.   suspend !inf 
  328.   close(inf)
  329. end
  330.  
  331.     # Query opens stdin because the system call to the editor
  332.     # redirects input.  The optional parameter is a default
  333.     # response if the user answers with <return>.
  334.  
  335. procedure query(prompt, def)
  336.   local ans
  337.   static stdin
  338.  
  339.   initial stdin := open(console)
  340.   writes(prompt)
  341.   ans := read(stdin)
  342.   return (*ans = 0 & \def) | ans
  343. end
  344.  
  345.     # A quick and dirty kludge.  Validate() builds a sorted list.
  346.     # When an element is found, it is popped and the search moves
  347.     # to the next item.  The procedure assumes the file is also
  348.     # sorted.
  349.  
  350. procedure validate(what, where)
  351.   local valid, stuff, sf, a
  352.  
  353.   mode ~== "local" & return what
  354.   valid := &letters ++ '.-' ++ &digits
  355.   stuff := []
  356.   what ? while tab(upto(valid)) do put(stuff,tab(many(valid))) 
  357.   sf := open(newslib || where) | {
  358.     remove(tmpfile)
  359.     stop("post: cannot open ", newslib || where)
  360.   }
  361.   stuff := sort(stuff)
  362.   a := pop(stuff)
  363.   every !sf ? match(a) & (a := pop(stuff)) | return what
  364.   remove(tmpfile)
  365.   stop("`", a, "' is not in ", newslib || where)
  366. end
  367.