home *** CD-ROM | disk | FTP | other *** search
- # nntp.tcl --
- #
- # nntp implementation for Tcl.
- #
- # Copyright (c) 1998-2000 by Ajuba Solutions.
- # All rights reserved.
- #
- # RCS: @(#) $Id: nntp.tcl,v 1.5 2001/08/02 16:38:07 andreas_kupries Exp $
-
- package require Tcl 8.2
- package provide nntp 0.1
-
- namespace eval ::nntp {
- # The socks variable holds the handle to the server connections
- variable socks
-
- # The counter is used to help create unique connection names
- variable counter 0
-
- # commands is the list of subcommands recognized by nntp
- variable commands [list \
- "article" \
- "authinfo" \
- "body" \
- "date" \
- "group" \
- "head" \
- "help" \
- "last" \
- "list" \
- "listgroup" \
- "mode_reader" \
- "newgroups" \
- "newnews" \
- "next" \
- "post" \
- "stat" \
- "quit" \
- "xgtitle" \
- "xhdr" \
- "xover" \
- "xpat" \
- ]
-
- set ::nntp::eol "\n"
-
- # only export one command, the one used to instantiate a new
- # nntp connection
- namespace export nntp
-
- }
-
- # ::nntp::nntp --
- #
- # Create a new nntp connection.
- #
- # Arguments:
- # server - The name of the nntp server to connect to (optional).
- # port - The port number to connect to (optional).
- # name - The name of the nntp connection to create (optional).
- #
- # Results:
- # Creates a connection to the a nntp server. By default the
- # connection is established with the machine 'news' at port '119'
- # These defaults can be overridden with the environment variables
- # NNTPPORT and NNTPHOST, or can be passed as optional arguments
-
- proc ::nntp::nntp {{server ""} {port ""} {name ""}} {
- global env
- variable connections
- variable counter
- variable socks
-
- # If a name wasn't specified for the connection, create a new 'unique'
- # name for the connection
-
- if { [llength [info level 0]] < 4 } {
- set counter 0
- set name "nntp${counter}"
- while {[lsearch -exact [info commands] $name] >= 0} {
- incr counter
- set name "nntp${counter}"
- }
- }
-
- if { ![string equal [info commands ::$name] ""] } {
- error "command \"$name\" already exists, unable to create nntp connection"
- }
-
- upvar 0 ::nntp::${name}data data
-
- set socks($name) [list ]
-
- # Initialize instance specific variables
-
- set data(debug) 0
- set data(eol) "\n"
-
- # Logic to determine whether to use the specified nntp server, or to use
- # the default
-
- if {$server == ""} {
- if {[info exists env(NNTPSERVER)]} {
- set data(host) "$env(NNTPSERVER)"
- } else {
- set data(host) "news"
- }
- } else {
- set data(host) $server
- }
-
- # Logic to determine whether to use the specified nntp port, or to use the
- # default.
-
- if {$port == ""} {
- if {[info exists env(NNTPPORT)]} {
- set data(port) $env(NNTPPORT)
- } else {
- set data(port) 119
- }
- } else {
- set data(port) $port
- }
-
- set data(code) 0
- set data(mesg) ""
- set data(addr) ""
-
- set sock [socket $data(host) $data(port)]
-
- set data(sock) $sock
-
- # Create the command to manipulate the nntp connection
-
- interp alias {} ::$name {} ::nntp::NntpProc $name
-
- ::nntp::response $name
-
- return $name
- }
-
- # ::nntp::NntpProc --
- #
- # Command that processes all nntp object commands.
- #
- # Arguments:
- # name name of the nntp object to manipulate.
- # args command name and args for the command.
- #
- # Results:
- # Calls the appropriate nntp procedure for the command specified in
- # 'args' and passes 'args' to the command/procedure.
-
- proc ::nntp::NntpProc {name {cmd ""} args} {
-
- # Do minimal args checks here
-
- if { [llength [info level 0]] < 3 } {
- error "wrong # args: should be \"$name option ?arg arg ...?\""
- }
-
- # Split the args into command and args components
-
- if { [llength [info commands ::nntp::_$cmd]] == 0 } {
- variable commands
- set optlist [join $commands ", "]
- set optlist [linsert $optlist "end-1" "or"]
- error "bad option \"$cmd\": must be $optlist"
- }
-
- # Call the appropriate command with its arguments
-
- return [eval [list ::nntp::_$cmd $name] $args]
- }
-
- # ::nntp::okprint --
- #
- # Used to test the return code stored in data(code) to
- # make sure that it is alright to right to the socket.
- #
- # Arguments:
- # name name of the nntp object.
- #
- # Results:
- # Either throws an error describing the failure, or
- # 'args' and passes 'args' to the command/procedure or
- # returns 1 for 'OK' and 0 for error states.
-
- proc ::nntp::okprint {name} {
- upvar 0 ::nntp::${name}data data
-
- if {$data(code) >=400} {
- set val [expr {(0 < $data(code)) && ($data(code) < 400)}]
- error "NNTPERROR: $data(code) $data(mesg)"
- }
-
- # Codes less than 400 are good
-
- return [expr {(0 < $data(code)) && ($data(code) < 400)}]
- }
-
- # ::nntp::message --
- #
- # Used to format data(mesg) for printing to the socket
- # by appending the appropriate end of line character which
- # is stored in data(eol).
- #
- # Arguments:
- # name name of the nntp object.
- #
- # Results:
- # Returns a string containing the message from data(mesg) followed
- # by the eol character(s) stored in data(eol)
-
- proc ::nntp::message {name} {
- upvar 0 ::nntp::${name}data data
-
- return "$data(mesg)$data(eol)"
- }
-
- #################################################
- #
- # NNTP Methods
- #
-
- # ::nntp::_article --
- #
- # Internal article proc. Called by the 'nntpName article' command.
- # Retrieves the article specified by msgid, in the group specified by
- # the 'nntpName group' command. If no msgid is specified the current
- # (or first) article in the group is retrieved
- #
- # Arguments:
- # name name of the nntp object.
- # msgid The article number to retrieve
- #
- # Results:
- # Returns the message (if there is one) from the specified group as
- # a valid tcl list where each element is a line of the message.
- # If no article is found, the "" string is returned.
- #
- # According to RFC 977 the responses are:
- #
- # 220 n article retrieved - head and body follow
- # (n = article number, = message-id)
- # 221 n article retrieved - head follows
- # 222 n article retrieved - body follows
- # 223 n article retrieved - request text separately
- # 412 no newsgroup has been selected
- # 420 no current article has been selected
- # 423 no such article number in this group
- # 430 no such article found
- #
-
- proc ::nntp::_article {name {msgid ""}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "fetch"
- return [::nntp::command $name "ARTICLE $msgid"]
- }
-
- # ::nntp::_authinfo --
- #
- # Internal authinfo proc. Called by the 'nntpName authinfo' command.
- # Passes the username and password for a nntp server to the nntp server.
- #
- # Arguments:
- # name Name of the nntp object.
- # user The username for the nntp server.
- # pass The password for 'username' on the nntp server.
- #
- # Results:
- # Returns the result of the attempts to set the username and password
- # on the nntp server ( 1 if successful, 0 if failed).
-
- proc ::nntp::_authinfo {name {user "guest"} {pass "foobar"}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) ""
- set res [::nntp::command $name "AUTHINFO USER $user"]
- if {$res} {
- set res [expr {$res && [::nntp::command $name "AUTHINFO PASS $pass"]}]
- }
- return $res
- }
-
- # ::nntp::_body --
- #
- # Internal body proc. Called by the 'nntpName body' command.
- # Retrieves the body of the article specified by msgid from the group
- # specified by the 'nntpName group' command. If no msgid is specified
- # the current (or first) message body is returned
- #
- # Arguments:
- # name Name of the nntp object.
- # msgid The number of the body of the article to retrieve
- #
- # Results:
- # Returns the body of article 'msgid' from the group specified through
- # 'nntpName group'. If msgid is not specified or is "" then the body of
- # the current (or the first) article in the newsgroup will be returned
- # as a valid tcl list. The "" string will be returned if there is no
- # article 'msgid' or if no group has been specified.
-
- proc ::nntp::_body {name {msgid ""}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "fetch"
- return [::nntp::command $name "BODY $msgid"]
- }
-
- # ::nntp::_group --
- #
- # Internal group proc. Called by the 'nntpName group' command.
- # Sets the current group on the nntp server to the group passed in.
- #
- # Arguments:
- # name Name of the nntp object.
- # group The name of the group to set as the default group.
- #
- # Results:
- # Sets the default group to the group specified. If no group is specified
- # or if an invalid group is specified an error is thrown.
- #
- # According to RFC 977 the responses are:
- #
- # 211 n f l s group selected
- # (n = estimated number of articles in group,
- # f = first article number in the group,
- # l = last article number in the group,
- # s = name of the group.)
- # 411 no such news group
-
- proc ::nntp::_group {name {group ""}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "groupinfo"
- if {$group == ""} {
- set group $data(group)
- }
- return [::nntp::command $name "GROUP $group"]
- }
-
- # ::nntp::_head --
- #
- # Internal head proc. Called by the 'nntpName head' command.
- # Retrieves the header of the article specified by msgid from the group
- # specified by the 'nntpName group' command. If no msgid is specified
- # the current (or first) message header is returned
- #
- # Arguments:
- # name Name of the nntp object.
- # msgid The number of the header of the article to retrieve
- #
- # Results:
- # Returns the header of article 'msgid' from the group specified through
- # 'nntpName group'. If msgid is not specified or is "" then the header of
- # the current (or the first) article in the newsgroup will be returned
- # as a valid tcl list. The "" string will be returned if there is no
- # article 'msgid' or if no group has been specified.
-
- proc ::nntp::_head {name {msgid ""}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "fetch"
- return [::nntp::command $name "HEAD $msgid"]
- }
-
- # ::nntp::_help --
- #
- # Internal help proc. Called by the 'nntpName help' command.
- # Retrieves a list of the valid nntp commands accepted by the server.
- #
- # Arguments:
- # name Name of the nntp object.
- #
- # Results:
- # Returns the NNTP commands expected by the NNTP server.
-
- proc ::nntp::_help {name} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "fetch"
- return [::nntp::command $name "HELP"]
- }
-
- proc ::nntp::_ihave {name {msgid ""} args} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "fetch"
- if {![::nntp::command $name "IHAVE $msgid"]} {
- return ""
- }
- return [::nntp::squirt $name "$args"]
- }
-
- # ::nntp::_last --
- #
- # Internal last proc. Called by the 'nntpName last' command.
- # Sets the current message to the message before the current message.
- #
- # Arguments:
- # name Name of the nntp object.
- #
- # Results:
- # None.
-
- proc ::nntp::_last {name} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "msgid"
- return [::nntp::command $name "LAST"]
- }
-
- # ::nntp::_list --
- #
- # Internal list proc. Called by the 'nntpName list' command.
- # Lists all groups or (optionally) all groups of a specified type.
- #
- # Arguments:
- # name Name of the nntp object.
- # Type The type of groups to return (active active.times newsgroups
- # distributions distrib.pats moderators overview.fmt
- # subscriptions) - optional.
- #
- # Results:
- # Returns a tcl list of all groups or the groups that match 'type' if
- # a type is specified.
-
- proc ::nntp::_list {name {type ""}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "fetch"
- return [::nntp::command $name "LIST $type"]
- }
-
- # ::nntp::_newgroups --
- #
- # Internal newgroups proc. Called by the 'nntpName newgroups' command.
- # Lists all new groups since a specified time.
- #
- # Arguments:
- # name Name of the nntp object.
- # since The time to find new groups since. The time can be in any
- # format that is accepted by 'clock scan' in tcl.
- #
- # Results:
- # Returns a tcl list of all new groups added since the time specified.
-
- proc ::nntp::_newgroups {name since args} {
- upvar 0 ::nntp::${name}data data
-
- set since [clock format [clock scan "$since"] -format "%y%m%d %H%M%S"]
- set dist ""
- set data(cmnd) "fetch"
- return [::nntp::command $name "NEWGROUPS $since $dist"]
- }
-
- # ::nntp::_newnews --
- #
- # Internal newnews proc. Called by the 'nntpName newnews' command.
- # Lists all new news in the specified group since a specified time.
- #
- # Arguments:
- # name Name of the nntp object.
- # group Name of the newsgroup to query.
- # since The time to find new groups since. The time can be in any
- # format that is accepted by 'clock scan' in tcl. Defaults to
- # "1 day ago"
- #
- # Results:
- # Returns a tcl list of all new messages since the time specified.
-
- proc ::nntp::_newnews {name {group ""} {since ""}} {
- upvar 0 ::nntp::${name}data data
-
- if {$group != ""} {
- if {[regexp -- {^[\w\.\-]+$} $group] == 0} {
- set since $group
- set group ""
- }
- }
- if {![info exists group] || ($group == "")} {
- if {[info exists data(group)] && ($data(group) != "")} {
- set group $data(group)
- } else {
- set group "*"
- }
- }
- if {"$since" == ""} {
- set since [clock format [clock scan "now - 1 day"]]
- }
- set since [clock format [clock scan $since] -format "%y%m%d %H%M%S"]
- set dist ""
- set data(cmnd) "fetch"
- return [::nntp::command $name "NEWNEWS $group $since $dist"]
- }
-
- # ::nntp::_next --
- #
- # Internal next proc. Called by the 'nntpName next' command.
- # Sets the current message to the next message after the current message.
- #
- # Arguments:
- # name Name of the nntp object.
- #
- # Results:
- # None.
-
- proc ::nntp::_next {name} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "msgid"
- return [::nntp::command $name "NEXT"]
- }
-
- # ::nntp::_post --
- #
- # Internal post proc. Called by the 'nntpName post' command.
- # Posts a message to a newsgroup.
- #
- # Responses (according to RFC 977) to a post request:
- # 240 article posted ok
- # 340 send article to be posted. End with .
- # 440 posting not allowed
- # 441 posting failed
- #
- # Arguments:
- # name Name of the nntp object.
- # args A message of the form specified in RFC 850
- #
- # Results:
- # None.
-
- proc ::nntp::_post {name args} {
-
- if {![::nntp::command $name "POST"]} {
- return ""
- }
- return [::nntp::squirt $name "$args"]
- }
-
- # ::nntp::_slave --
- #
- # Internal slave proc. Called by the 'nntpName slave' command.
- # Identifies a connection as being made from a slave nntp server.
- # This might be used to indicate that the connection is serving
- # multiple people and should be given priority. Actual use is
- # entirely implementation dependant and may vary from server to
- # server.
- #
- # Arguments:
- # name Name of the nntp object.
- #
- # Results:
- # None.
- #
- # According to RFC 977 the only response is:
- #
- # 202 slave status noted
-
- proc ::nntp::_slave {name} {
- return [::nntp::command $name "SLAVE"]
- }
-
- # ::nntp::_stat --
- #
- # Internal stat proc. Called by the 'nntpName stat' command.
- # The stat command is similar to the article command except that no
- # text is returned. When selecting by message number within a group,
- # the stat command serves to set the current article pointer without
- # sending text. The returned acknowledgement response will contain the
- # message-id, which may be of some value. Using the stat command to
- # select by message-id is valid but of questionable value, since a
- # selection by message-id does NOT alter the "current article pointer"
- #
- # Arguments:
- # name Name of the nntp object.
- # msgid The number of the message to stat (optional) default is to
- # stat the current article
- #
- # Results:
- # Returns the statistics for the article.
-
- proc ::nntp::_stat {name {msgid ""}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "status"
- return [::nntp::command $name "STAT $msgid"]
- }
-
- # ::nntp::_quit --
- #
- # Internal quit proc. Called by the 'nntpName quit' command.
- # Quits the nntp session and closes the socket. Deletes the command
- # that was created for the connection.
- #
- # Arguments:
- # name Name of the nntp object.
- #
- # Results:
- # Returns the return value from the quit command.
-
- proc ::nntp::_quit {name} {
- upvar 0 ::nntp::${name}data data
-
- set ret [::nntp::command $name "QUIT"]
- close $data(sock)
- rename ${name} {}
- return $ret
- }
-
- #############################################################
- #
- # Extended methods (not available on all NNTP servers
- #
-
- proc ::nntp::_date {name} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "msg"
- return [::nntp::command $name "DATE"]
- }
-
- proc ::nntp::_listgroup {name {group ""}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "fetch"
- return [::nntp::command $name "LISTGROUP $group"]
- }
-
- proc ::nntp::_mode_reader {name} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "msg"
- return [::nntp::command $name "MODE READER"]
- }
-
- proc ::nntp::_xgtitle {name {group_pattern ""}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "fetch"
- return [::nntp::command $name "XGTITLE $group_pattern"]
- }
-
- proc ::nntp::_xhdr {name {header "message-id"} {list ""} {last ""}} {
- upvar 0 ::nntp::${name}data data
-
- if {![regexp -- {\d+-\d+} $list]} {
- if {"$last" != ""} {
- set list "$list-$last"
- } else {
- set list ""
- }
- }
- set data(cmnd) "fetch"
- return [::nntp::command $name "XHDR $header $list"]
- }
-
- proc ::nntp::_xindex {name {group ""}} {
- upvar 0 ::nntp::${name}data data
-
- if {("$group" == "") && [info exists data(group)]} {
- set group $data(group)
- }
- set data(cmnd) "fetch"
- return [::nntp::command $name "XINDEX $group"]
- }
-
- proc ::nntp::_xmotd {name {since ""}} {
- upvar 0 ::nntp::${name}data data
-
- if {"$since" != ""} {
- set since [clock seconds]
- }
- set since [clock format [clock scan $since] -format "%y%m%d %H%M%S"]
- set data(cmnd) "fetch"
- return [::nntp::command $name "XMOTD $since"]
- }
-
- proc ::nntp::_xover {name {list ""} {last ""}} {
- upvar 0 ::nntp::${name}data data
- if {![regexp -- {\d+-\d+} $list]} {
- if {"$last" != ""} {
- set list "$list-$last"
- } else {
- set list ""
- }
- }
- set data(cmnd) "fetch"
- return [::nntp::command $name "XOVER $list"]
- }
-
- proc ::nntp::_xpat {name {header "subject"} {list 1} {last ""} args} {
- upvar 0 ::nntp::${name}data data
-
- set patterns ""
-
- if {![regexp -- {\d+-\d+} $list]} {
- if {("$last" != "") && ([string is digit $last])} {
- set list "$list-$last"
- }
- } elseif {"$last" != ""} {
- set patterns "$last"
- }
-
- if {[llength $args] > 0} {
- set patterns "$patterns $args"
- }
-
- if {"$patterns" == ""} {
- set patterns "*"
- }
-
- set data(cmnd) "fetch"
- return [::nntp::command $name "XPAT $header $list $patterns"]
- }
-
- proc ::nntp::_xpath {name {msgid ""}} {
- upvar 0 ::nntp::${name}data data
-
- set data(cmnd) "msg"
- return [::nntp::command $name "XPATH $msgid"]
- }
-
- proc ::nntp::_xsearch {name args} {
- set res [::nntp::command $name "XSEARCH"]
- if {!$res} {
- return ""
- }
- return [::nntp::squirt $name "$args"]
- }
-
- proc ::nntp::_xthread {name args} {
- upvar 0 ::nntp::${name}data data
-
- if {[llength $args] > 0} {
- set filename "dbinit"
- } else {
- set filename "thread"
- }
- set data(cmnd) "fetchbinary"
- return [::nntp::command $name "XTHREAD $filename"]
- }
-
- ######################################################
- #
- # Helper methods
- #
-
- proc ::nntp::cmd {name cmd} {
- upvar 0 ::nntp::${name}data data
-
- set eol "\015\012"
- set sock $data(sock)
- if {$data(debug)} {
- puts stderr "$sock command $cmd"
- }
- puts $sock "$cmd"
- flush $sock
- return
- }
-
- proc ::nntp::command {name args} {
- set res [eval [list ::nntp::cmd $name] $args]
-
- return [::nntp::response $name]
- }
-
- proc ::nntp::msg {name} {
- upvar 0 ::nntp::${name}data data
-
- set res [::nntp::okprint $name]
- if {!$res} {
- return ""
- }
- return $data(mesg)
- }
-
- proc ::nntp::groupinfo {name} {
- upvar 0 ::nntp::${name}data data
-
- set data(group) ""
-
- if {[::nntp::okprint $name] && [regexp -- {(\d+)\s+(\d+)\s+(\d+)\s+([\w\.]+)} \
- $data(mesg) match count first last data(group)]} {
- return [list $count $first $last $data(group)]
- }
- return ""
- }
-
- proc ::nntp::msgid {name} {
- upvar 0 ::nntp::${name}data data
-
- set result ""
- if {[::nntp::okprint $name] && \
- [regsub -- {\s+<[^>]+>} $data(mesg) {} result]} {
- return $result
- } else {
- return ""
- }
- }
-
- proc ::nntp::status {name} {
- upvar 0 ::nntp::${name}data data
-
- set result ""
- if {[::nntp::okprint $name] && \
- [regexp -- {\d+\s+<[^>]+>} $data(mesg) result]} {
- return $result
- } else {
- return ""
- }
- }
-
- proc ::nntp::fetch {name} {
- upvar 0 ::nntp::${name}data data
-
- set eol "\012"
-
- if {![::nntp::okprint $name]} {
- return ""
- }
- set sock $data(sock)
-
- set result [list ]
- while {![eof $sock]} {
- gets $sock line
- regsub -- {\015?\012$} $line $data(eol) line
-
- if {[regexp -- {^\.$} $line]} {
- break
- }
- regsub -- {^\.\.} $line {.} line
- lappend result $line
- }
- return $result
- }
-
- proc ::nntp::response {name} {
- upvar 0 ::nntp::${name}data data
-
- set eol "\012"
-
- set sock $data(sock)
-
- gets $sock line
- set data(code) 0
- set data(mesg) ""
-
- if {$line == ""} {
- error "nntp: unexpected EOF on $sock\n"
- }
-
- regsub -- {\015?\012$} $line "" line
-
- set result [regexp -- {^((\d\d)(\d))\s*(.*)} $line match \
- data(code) val1 val2 data(mesg)]
-
- if {$result == 0} {
- puts stderr "nntp garbled response: $line\n";
- return ""
- }
-
- if {$val1 == 20} {
- set data(post) [expr {!$val2}]
- }
-
- if {$data(debug)} {
- puts stderr "val1 $val1 val2 $val2"
- puts stderr "code '$data(code)'"
- puts stderr "mesg '$data(mesg)'"
- if {[info exists data(post)]} {
- puts stderr "post '$data(post)'"
- }
- }
-
- return [::nntp::returnval $name]
- }
-
- proc ::nntp::returnval {name} {
- upvar 0 ::nntp::${name}data data
-
- if {([info exists data(cmnd)]) \
- && ($data(cmnd) != "")} {
- set command $data(cmnd)
- } else {
- set command okprint
- }
-
- if {$data(debug)} {
- puts stderr "returnval command '$command'"
- }
-
- set data(cmnd) ""
- return [::nntp::$command $name]
- }
-
- proc ::nntp::squirt {name {body ""}} {
- upvar 0 ::nntp::${name}data data
-
- set body [split $body \n]
-
- if {$data(debug)} {
- puts stderr "$data(sock) sending [llength $body] lines\n";
- }
-
- foreach line $body {
- # Print each line, possibly prepending a dot for lines
- # starting with a dot and trimming any trailing \n.
- regsub -- {^\.} $line {..} line
- puts $data(sock) $line
- }
- puts $data(sock) "."
- flush $data(sock)
-
- if {$data(debug)} {
- puts stderr "$data(sock) is finished sending"
- }
- return [::nntp::response $name]
- }
- #eof
-
-