Simple News Posting Program

I came into work today and found that inews had stopped working, cutting me off from participating in news:comp.lang.tcl to my great annoyance. So I started posting by connecting directly to the NNTP server, but that is a bit of a drag. So I wrote this script to automate the process. Enjoy! Donal Fellows

PS: Feel free to augment it with the bits that are missing (password negotiation for servers that require authentication, and better MIME handling are the main missing features.)

 # This is a UNIX-specific script; it will not work on other platforms... \
 exec tclsh $0 ${1+"$@"}

 ### This script is a simple replacement for inews, the standard
 ### Unix USENET posting command.  It does simple, though easy to
 ### customise, header processing, and it makes sure that the body
 ### of messages gets sent through unmangled.  Or something like that.

 set host myNNTPserver;# I've got to set it to *something*
 if {[info exist env(NNTPSERVER)]} {set host $env(NNTPSERVER)}
 set port nntp;# NNTP port, which is 119 normally
 # Guess the email address.  This works for me...
 set emailAddress $env(LOGNAME)@[exec /usr/bin/domainname]
 # And guess what I want to be known as.  Again, this works for me...
 set realName $env(NAME)

 # Ought to process arguments here!

 proc capitalise1 {word} {
     set c0 [string index $word 0]
     set cr [string range $word 1 end]
     return [string toupper $c0][string tolower $cr]
 proc capitalise {string} {
     set result {}
     foreach word [split $string "-"] {lappend result [capitalise1 $word]}
     join $result "-"

 # Read NNTP response message.  Primitive, but works!
 proc readResponse {{expected ""}} {
     global nntp
     upvar code code message message
     flush $nntp
     set len [gets $nntp line]
     while {$len == 0} {
         set len [gets $nntp line]
     if {$len < 0} {
         error "OOPS!  Unexpected closure of NNTP socket"
     if {![regexp {^([0-9]+) +(.*)} $line => code message]} {
         error "OOPS!  Bad response: $line"
     if {[string length $expected] && [string compare $code $expected]} {
         return -code error "Unexpected reply: $message"

 # Post a message.  Note that the headers are an array, and the body a list.
 proc post {} {
     global nntp head body
     puts $nntp "post"
     if {$code != 340} {
         return -code error "Cannot post: $message"
     foreach {header value} [array get head] {
         puts $nntp "[capitalise $header]: [string trim $value]"
     puts $nntp ""
     foreach line $body {
         if {[string index $line 0] == "."} {
             puts $nntp ".$line"
         } else {
             puts $nntp $line
     puts $nntp .
     if {$code != 240} {
         return -code error "Failed to post: $message"

 # Connecting to, and disconnecting from an NNTP server.
 proc connect {host {port 119}} {
     global nntp
     set nntp [socket $host $port]
     readResponse 200
 proc quit {} {
     global nntp
     puts $nntp "quit"
     readResponse 205
     close $nntp

 # Split up a message in pseudo-RFC822 format for more processing
 proc splitMessage {message} {
     global head body
     set inBody 0
     array set head {}
     set body {}
     set lastheader {}
     foreach line [split $message "\n"] {
         if {$inBody} {
             lappend body $line
         } elseif {[string length $line] == 0} {
             set inBody 1
         } elseif {[regexp {^([^ :]+): +(.*)} $line => header value]} {
             set header [string tolower $header]
             set value [string trim $value]
             if {[string length $value]} {
                 set head($header) "$value "
             set lastheader $header
         } else {
             append head($lastheader) "[string trim $line] "

 # Specify that the given header must be specified in the input message
 proc needHeader {header} {
     global head
     if {![info exist head([string tolower $header])]} {
         return -code error "Required header \"${header}:\" is missing"
 # Add the given header to the message to be posted
 proc addHeader {header value} {
     global head
     set header [string tolower $header]
     if {![info exist head($header)]} {
         set head($header) $value

 # Debugging wrapped up for neatness!
 proc DEBUG {} {
     global head body
     parray head
     puts "--------------------------------"
     puts [join $body "\n"]
     exit 0

 # ---------------------------------------------------------------------
 # Now we get to the business end of things.  We start by reading in the
 # message to be posted.
 splitMessage [string trimright [read stdin]]
 # Some headers are required
 needHeader Newsgroups
 needHeader Subject
 # And others we can supply
 addHeader From "$realName <$emailAddress>"
 addHeader Organization \
             "Department of Computer Science, University of Manchester"
 addHeader "X-Posting-Engine" "postnntp v0.02a on Tcl [info patchlevel]"
 # My news-server handles the adding of the Lines: header itself
 #addHeader Lines [llength $body]
 addHeader "Content-Type" "text/plain; charset=iso-8859-1"

 # If you want to add MIME encoding of the body or headers, this is the
 # place to do it.

 # Uncomment the next line when debugging the message reading/generating

 # Now post the message.  Simple, isn't it?
 connect $host $port
 # You might need to add some stuff here for authentication on some servers.

 # That's all, folks!
 exit 0