/* FARGOS Development, LLC Sample Programs Copyright (C) 2002 FARGOS Development, LLC. All rights reserved. mailto:support@fargos.net for assistance. NOTE: in the future, enhanced versions of these classes may be added to the FARGOS/VISTA Object Management Environment core within the Standard namespace. Developers can avoid any potential conflict if they specify an explicit namespace when creating instances of the sample classes. */ %include // References: RFC 977 (NNTP), RFC 1036 (USENET Message Format), // RFC 2980 (NTTP Extensions) // RFC 1738: news:news.group.name // news:message-ID -- no "<" ">" include, distinguished by presence of "@" // nntp://[:=119]/newsgroupName/articleNumber // IETF draft: // news:[//[:port]/]newsgroupName/article[-lastArticle] // news:[//[:port]/]localMessId@domain global NNTP { const string NNTP_SERVER_ID = "FARGOS/VISTA NNTP Server 1.0"; const int MAX_IDLE_TIME = 300; }; class Local . NNTPserver { assoc configParams; oid listenObj; int connectionsAccepted; } inherits from Object; NNTPserver:create(string listenAtAddr, string hostName) { assoc acl, connACL; string nntpHostName, listenAddress; if (length(hostName) > 0) { nntpHostName = hostName; } else { nntpHostName = getSystemInfoAttribute("hostName"); } configParams["hostName"] = nntpHostName; configParams["postingOK"] = 1; if (typeOf(listenAtAddr) == string) { listenAddress = listenAtAddr; } else { listenAddress = "tcp:0.0.0.0:119,l"; } connACL = makePermitEveryoneACL(); acl = makeDefaultACL(); listenObj = send "createObject"("AcceptConnection", acl, listenAddress, thisObject, connACL) to ObjectCreator; send "addNotifyOnShutdown"(thisObject) to "ShutdownService"; debugDisplay(debugLogLevel3, "NNTPserver operating on ", listenAddress, "\n"); } NNTPserver:delete() { if (listenObj != nil) send "deleteYourself" to listenObj; } NNTPserver:systemShutdown() { /*! Because it provides a long-running service, class=NNTPserver objects register themselves with the class=ShutdownService, which sends a method=systemShutdown notification when a graceful shutdown is requested. !*/ // TO DO: consider deletion without shutdown... send "deleteYourself" to thisObject; } NNTPserver:connectionAccepted(oid newSocket) { /*! When new HTTP connections are received, the method=connectionAccepted method creates a class=HTTPfastReceive object to handle the I/O and processing of any requests. !*/ assoc acl; // becomePseudoUser(); connectionsAccepted += 1; acl = makeDefaultACL(); send "createObject"("NNTPclientConnection", acl, newSocket, configParams) to ObjectCreator from nil; // don't bother with response } class Local . NNTPclientConnection { enum ParseStates { HELLO, POST, IHAVE }; oid connObj; oid readBfr; oid newsgroupService; string peerAddress; // string claimedPeerName; int lastRequestTime; assoc serverConfig; oid timerObj; int currentParseState; set articleBody; int sessionClosed; oid shutdownOID; string currentNewsgroupName; oid currentNewsgroupObj; any currentArticleInfo; any userName; } inherits from Object; NNTPclientConnection:create(oid newSocket, assoc configParams) { assoc acl, currentTime; int t, rc; string date, initialLine, okCode; connObj = newSocket; serverConfig = configParams; peerAddress = send "getPeerAddress" to connObj; debugDisplay(debugLogLevel3, "New NNTP connection from ", peerAddress, "\n"); newsgroupService = lookupLocalService("NewsgroupDirectory"); if (newsgroupService == nil) { send "writeBytes"("400 service closed - no NewsgroupDirectory\r\n") to connObj from nil; send "deleteYourself" to thisObject; exit; } lastRequestTime = getLocalRelativeTime(); currentTime = convertLocalRelativeTimeToAbsolute(lastRequestTime, 0); date = rfc1123Date(currentTime); if (serverConfig["postingOK"] == 1) okCode = "200 "; else okCode = "201 "; initialLine = makeAsString(okCode, serverConfig["hostName"], " ", NNTP_SERVER_ID, "; local time is ", date, "\r\n"); rc = send "writeBytes"(initialLine) to connObj; if (rc <= 0) { display("Could not announce to client: ", initialLine, "\n"); send "deleteYourself" to connObj; send "deleteYourself" to thisObject; exit; } currentParseState = HELLO; acl = makeDefaultACL(); readBfr = send "createObject"("ReadBuffer", acl, connObj) to ObjectCreator; // NOTE: end of line delimeter must be set to CR/LF for correctness. // RFC 2821, section 4.1.1.4 requires that a single LF MUST NOT // be acccepted, even if it would seem to tolerate incorrect // implementations. This is one case where being tolerant in // what one accepts is explicitly disallowed. send "setDelimeter"("\r\n") to readBfr; send "selectForRead" to readBfr; timerObj = send "createObject"("TimerEvent", acl, thisObject, MAX_IDLE_TIME) to ObjectCreator; // TO DO: only permit systemShutdown method shutdownOID = thisObject; send "addNotifyOnShutdown"(shutdownOID) to "ShutdownService"; } NNTPclientConnection:delete() { if (readBfr != nil) send "deleteYourself" to readBfr; if (shutdownOID != nil) { send "removeNotifyOnShutdown"(shutdownOID) to "ShutdownService"; } } NNTPclientConnection:timerExpired(oid timerObj) { assoc acl; int t, delta; if (sessionClosed == 1) { display("Session was closed, time to delete\n"); send "deleteYourself" to thisObject; exit; } t = getLocalRelativeTime(); delta = t - lastRequestTime; if (delta > MAX_IDLE_TIME) { display("NNTP client connection with ", peerAddress, " timed out\n"); send "deleteYourself" to thisObject; exit; } acl = makeDefaultACL(); timerObj = send "createObject"("TimerEvent", acl, thisObject, MAX_IDLE_TIME, t) to ObjectCreator; } NNTPclientConnection:canRead(oid bfr) { any line, tokens; string cmd; int rc; lastRequestTime = getLocalRelativeTime(); line = send "readLine" to readBfr; display("NNTP client line=", line, "\n"); if (line == nil) { // EOF display("EOF from NNTP client ", peerAddress, "\n"); send "deleteYourself" to readBfr; readBfr = nil; sessionClosed = 1; exit; } if (currentParseState == POST) { //display("accepting message body\n"); if (line != ".") { if (length(line) > 0) { if (midchar(line, 0) != '.') articleBody += line; else { // delete initial period articleBody += midstr(line, 1, length(line) - 1); } } articleBody += "\r\n"; } else { // process mail message... call "_processPOST"(makeAsString(articleBody)); currentParseState = HELLO; } } else { tokens = tokenizeString(line, " \t\r\n", 0); //display("tokenized line=", tokens); if (elementCount(tokens) > 0) { cmd = convertCase(tokens[0], 0); } else { cmd = ""; // bogus command } if (cmd == "ARTICLE") { call "_article"(0, tokens); } else if (cmd == "HEAD") { call "_article"(1, tokens); } else if (cmd == "BODY") { call "_article"(2, tokens); } else if (cmd == "STAT") { call "_article"(3, tokens); } else if (cmd == "GROUP") { call "_group"(tokens); } else if (cmd == "HELP") { call "_help"(tokens); } else if (cmd == "IHAVE") { call "_iHave"(tokens); } else if (cmd == "LAST") { call "_last"(tokens); } else if (cmd == "LIST") { call "_list"(tokens); } else if (cmd == "NEWGROUPS") { call "_newGroups"(tokens); } else if (cmd == "NEWNEWS") { call "_newNews"(tokens); } else if (cmd == "NEXT") { call "_next"(tokens); } else if (cmd == "POST") { call "_post"(tokens); } else if (cmd == "QUIT") { line = makeAsString("205 ", serverConfig["hostName"], " is now closing transmission channel\r\n"); send "writeBytes"(line) to connObj from nil; send "deleteYourself" to readBfr; readBfr = nil; sessionClosed = 1; exit; } else if (cmd == "SLAVE") { line = makeAsString("202 ", serverConfig["hostName"], " acknowledges your SLAVE status\r\n"); send "writeBytes"(line) to connObj from nil; } // RFC 2980 extensions else if (cmd == "MODE") { // MODE STREAM, MODE READER call "_mode"(tokens); } else if (cmd == "XOVER") { call "_xover"(tokens); } else if (cmd == "AUTHINFO") { call "_authInfo"(tokens); /* } else if (cmd == "CHECK") { } else if (cmd == "TAKETHIS") { } else if (cmd == "XREPLIC") { } else if (cmd == "LISTGROUP") { } else if (cmd == "XGTITLE") { } else if (cmd == "XHDR") { } else if (cmd == "XINDEX") { } else if (cmd == "XPAT") { } else if (cmd == "XPATH") { } else if (cmd == "XROVER") { } else if (cmd == "XTHREAD") { } else if (cmd == "XTHREAD") { } else if (cmd == "DATE") { } else if (cmd == "WILDMAT") { } else if (cmd == "WILDMAT") { */ } else if (cmd == "NOOP") { send "writeBytes"("250 OK Did nothing\r\n") to connObj from nil; } else { display("UNSUPPORTED COMMAND ", cmd, "\n"); if (cmd != "") { send "writeBytes"("500 Command " + cmd + " not implemented\r\n") to connObj from nil; } } } send "selectForRead" to readBfr; } NNTPclientConnection:_help(array tokens) { set lines; string l; lines += "100 Help text follows\r\n"; lines += "FARGOS/VISTA NNTP Server\r\n"; lines += "Commands: ARTICLE BODY HEAD STAT\r\n"; lines += " VRFY EXPN\r\n"; lines += " RSET NOOP HELP\r\n"; lines += "End of HELP output\r\n"; lines += ".\r\n"; l = makeAsString(lines); send "writeBytes"(l) to connObj from nil; return (0); } // RFC 977 - Section 3.1.2 NNTPclientConnection:_article(int showFlags, array tokens) { int id; any arg, articleInfo, hdrLines, bodyLines; set lines; if (indexExists(tokens, 1) != 0) { // argument provided arg = tokens[1]; if (findSubstring(arg, "@") != -1) { // message Id if (midchar(arg, 0) != '<') { // must add "<", ">" arg = makeAsString("<", arg, ">"); } articleInfo = send "findArticleWithMessageId"(arg, currentNewsgroupName) to newsgroupService; if (articleInfo == nil) { send "writeBytes"("430 No article with message Id found\r\n") to connObj from nil; return (0); } // NOTE: DO NOT UPDATE CURRENT ARTICLE } else { // select using article number if (currentNewsgroupObj == nil) { // not selected send "writeBytes"("412 No newsgroup has yet been selected\r\n") to connObj from nil; return (0); } id = stringToNumber(arg, int); articleInfo = send "findArticleNumber"(id, currentNewsgroupName) to currentNewsgroupObj; if (articleInfo == nil) { send "writeBytes"("423 No such article number in current newsgroup\r\n") to connObj from nil; return (0); } currentArticleInfo = articleInfo; // update } } else { // no argument, use current article if (currentArticleInfo == nil) { send "writeBytes"("420 No article has yet been selected\r\n") to connObj from nil; return (0); } articleInfo = currentArticleInfo; } // RC articleId explanation text lines += makeAsString(220 + showFlags, " ", articleInfo[1], " ", articleInfo[2], " article retrieved - "); if ((showFlags == 0) || (showFlags == 1)) lines += "head "; if ((showFlags == 0) || (showFlags == 2)) lines += "body "; if (showFlags == 3) lines += " request text separately\r\n"; else lines += " to follow\r\n"; if ((showFlags == 0) || (showFlags == 1)) { hdrLines = send "getHeaderLines"() to articleInfo[0]; lines |= hdrLines; } if ((showFlags == 0) || (showFlags == 2)) { if (showFlags == 0) lines += "\r\n"; // separate header/body bodyLines = send "getArticleBody"(1) to articleInfo[0]; lines |= bodyLines; } if (showFlags != 3) { // some text was sent lines += ".\r\n"; // end of response } send "writeVectorOfBytes"(0, lines) to connObj from nil; return (0); } NNTPclientConnection:_group(array tokens) { oid groupObj; any info; string line; groupObj = send "findNewsgroup"(tokens[1]) to newsgroupService; if (groupObj == nil) { send "writeBytes"("411 No such newsgroup\r\n") to connObj from nil; return (0); } info = send "getNewsgroupInfo" to groupObj; line = makeAsString("211 ", info["articleCount"], " ", info["firstArticle"], " ", info["lastArticle"], " ", info["newsgroupName"], " Group selected\r\n"); currentNewsgroupObj = groupObj; currentArticleInfo = nil; // none now selected send "writeBytes"(line) to connObj from nil; return (0); } NNTPclientConnection:_iHave(array tokens) { any articleInfo; if (serverConfig["postingOK"] != 1) { send "writeBytes"("437 Rejected - please do not try again\r\n") to connObj from nil; return (0); } articleInfo = send "findArticleWithMessageId"(tokens[1], currentNewsgroupName) to newsgroupService; if (articleInfo != nil) { // already present send "writeBytes"("435 Already have article with message Id\r\n") to connObj from nil; return (0); } send "writeBytes"("335 Please send article, end with .\r\n") to connObj from nil; currentParseState = IHAVE; return (0); } NNTPclientConnection:_post(array tokens) { if (serverConfig["postingOK"] != 1) { send "writeBytes"("440 Posting not permitted\r\n") to connObj from nil; return (0); } send "writeBytes"("340 Please send article for posting, end with .\r\n") to connObj from nil; currentParseState = POST; articleBody = emptySet; return (0); } NNTPclientConnection:_processPOST(string data) { assoc acl; oid obj; string className; int rc; if (indexExists(serverConfig, "articleClass") != 0) { className = serverConfig["articleClass"]; } else { className = "NNTParticle"; } display("Post using CLASS NAME=", className, "\n"); acl = makeDefaultACL(); obj = send "createObject"(className, acl, data) to ObjectCreator; rc = send "getPostingStatus" to obj; // -1 = cancelled, 0 = no valid newsgroups, 1 <= number of groups if (rc == 0) { send "writeBytes"("441 Posting failed\r\n") to connObj from nil; } else { if (rc == -1) send "deleteYourself" to obj; // cancelled send "writeBytes"("240 Article posted\r\n") to connObj from nil; } return (0); } NNTPclientConnection:_last(array tokens) { any info; string l; if (currentNewsgroupObj == nil) { send "writeBytes"("412 No newsgroup has yet been selected\r\n") to connObj from nil; return (0); } if (currentArticleInfo == nil) { send "writeBytes"("420 No article has yet been selected\r\n") to connObj from nil; return (0); } info = send "findArticleBefore"(currentArticleInfo[1]) to currentNewsgroupObj; if (info == nil) { send "writeBytes"("422 No previous article in this group\r\n") to connObj from nil; return (0); } l = makeAsString("223 ", info[1], " ", info[2], " Article retrieved - request text\r\n"); currentArticleInfo = info; send "writeBytes"(l) to connObj from nil; return (0); } NNTPclientConnection:_next(array tokens) { any info; string l; if (currentNewsgroupObj == nil) { send "writeBytes"("412 No newsgroup has yet been selected\r\n") to connObj from nil; return (0); } if (currentArticleInfo == nil) { send "writeBytes"("420 No article has yet been selected\r\n") to connObj from nil; return (0); } info = send "findArticleAfter"(currentArticleInfo[1]) to currentNewsgroupObj; if (info == nil) { send "writeBytes"("421 No next article in this group\r\n") to connObj from nil; return (0); } l = makeAsString("223 ", info[1], " ", info[2], " Article retrieved - request text\r\n"); currentArticleInfo = info; send "writeBytes"(l) to connObj from nil; return (0); } NNTPclientConnection:_list(array tokens) { any list, responseLines, arg; set lines, wildCards; int addDesc; if (indexExists(tokens, 1) != 0) { arg = convertCase(tokens[1], 0); if (arg == "NEWSGROUPS") { addDesc = 1; if (indexExists(tokens, 2) != 0) { wildCards += tokens[2]; } } if (arg == "ACTIVE") { // RFC 2980 - Sectopm 2.1.2 if (indexExists(tokens, 2) != 0) { wildCards += tokens[2]; } } } list = send "listNewsgroups"(wildCards) to newsgroupService; lines += "215 List of newsgroups follows\r\n"; responseLines = call "_outputGroups"(list, 0, addDesc); lines |= responseLines; lines += ".\r\n"; // end of list display("RESPONSE LINES:\n", lines); send "writeVectorOfBytes"(0, lines) to connObj from nil; return (0); } // NEWGROUPS yymmdd hhmmss [GMT] [] NNTPclientConnection:_newGroups(array tokens) { any list, responseLines; set lines; int t, n, i; string s; assoc dateInfo; t = getLocalRelativeTime(); dateInfo = convertLocalRelativeTimeToAbsolute(t, 0); if (indexExists(tokens, 1) != 0) { s = tokens[1]; } else { s = "010101"; // default 2001-Jan-01 } n = stringToNumber(midstr(s, 0, 2), int); dateInfo["year"] = 2000 + n; n = stringToNumber(midstr(s, 2, 2), int); dateInfo["month"] = n; n = stringToNumber(midstr(s, 4, 2), int); dateInfo["dayOfMonth"] = n; if (indexExists(tokens, 2) != 0) { s = tokens[2]; } else { s = "000000"; // default is midnight } n = stringToNumber(midstr(s, 0, 2), int); dateInfo["hours"] = n; n = stringToNumber(midstr(s, 2, 2), int); dateInfo["minutes"] = n; n = stringToNumber(midstr(s, 4, 2), int); dateInfo["seconds"] = n; i = 3; if (indexExists(tokens, i) != 0) { if (tokens[3] == "GMT") { i = 4; dateInfo["gmtDelta"] = 0; // GMT } } t = convertAbsoluteToLocalRelativeTime(dateInfo); // TO DO: filter on optional distribution specifications list = send "listNewsgroups" to newsgroupService; lines += "231 List of newsgroups follows\r\n"; responseLines = call "_outputGroups"(list, t, 0); lines |= responseLines; lines += ".\r\n"; // end of list send "writeVectorOfBytes"(0, lines) to connObj from nil; return (0); } NNTPclientConnection:_outputGroups(any groupList, int onlyAfter, int descOnly) { int i; oid obj; any info; set result; display("outGr argv=",argv); if (elementCount(groupList) == 0) return (""); if (indexExists(groupList, 0) != 0) i = 0; else i = nextIndex(groupList, 0); do { obj = groupList[i]; info = send "getNewsgroupInfo" to obj; if (onlyAfter != 0) { // filter on create time if (info["createTime"] < onlyAfter) { i = nextIndex(groupList, i); continue; } } result += info["newsgroupName"]; if (descOnly == 1) { // LIST NEWSGROUPS result += " "; result += info["description"]; result += "\r\n"; i = nextIndex(groupList, i); continue; } result += makeAsString(" ", info["lastArticle"], " ", info["firstArticle"], " "); if (serverConfig["postingOK"] != 1) { result += "n"; } else { if (info["postingOK"] != 1) result += "n"; else result += "y"; } result += "\r\n"; i = nextIndex(groupList, i); } while (i != 0); return (result); } // NEWNEWS newsgroups yymmdd hhmmss [GMT] [] NNTPclientConnection:_newNews(array tokens) { any list, responseLines; array newsgroups; set lines; int t, n, i; string s; assoc dateInfo; t = getLocalRelativeTime(); dateInfo = convertLocalRelativeTimeToAbsolute(t, 0); s = tokens[2]; n = stringToNumber(midstr(s, 0, 2), int); dateInfo["year"] = 2000 + n; n = stringToNumber(midstr(s, 2, 2), int); dateInfo["month"] = n; n = stringToNumber(midstr(s, 4, 2), int); dateInfo["dayOfMonth"] = s = tokens[3]; n = stringToNumber(midstr(s, 0, 2), int); dateInfo["hours"] = n; n = stringToNumber(midstr(s, 2, 2), int); dateInfo["minutes"] = n; n = stringToNumber(midstr(s, 4, 2), int); dateInfo["seconds"] = n; i = 4; if (indexExists(tokens, i) != 0) { if (tokens[4] == "GMT") { i = 5; dateInfo["gmtDelta"] = 0; // GMT } } t = convertAbsoluteToLocalRelativeTime(dateInfo); // TO DO: filter on optional distribution specifications newsgroups = tokenizeString(tokens[1], ",", 0); list = send "listNewsgroups"(arrayToSet(newsgroups)) to newsgroupService; lines += "230 List of articles follows\r\n"; responseLines = call "_outputArticles"(list, t); lines |= responseLines; lines += ".\r\n"; // end of list send "writeVectorOfBytes"(0, lines) to connObj from nil; return (0); } NNTPclientConnection:_outputArticles(any newsgroupList, int afterTime) { int i, j; oid ngObj; set lines; any articleIds; for(i=0;i != 0;i=nextIndex(newsgroupList, i)) { ngObj = newsgroupList[i]; articleIds = send "getMessageIds"(afterTime) to ngObj; for(j=nextIndex(articleIds, 0); j!=0;j=nextIndex(articleIds, j)) { lines += articleIds[j][1]; lines += "\r\n"; } } return (lines); } NNTPclientConnection:_mode(array tokens) { any m; string line; m = convertCase(tokens[1], 0); // RFC 2980 - Section 2.3 if (m == "READER") { if (serverConfig["postingOK"] == 1) { line = "200 Posting is permitted\r\n"; } else { line = "201 Posting is not permitted\r\n"; } send "writeBytes"(line) to connObj from nil; return (0); } // RFC 2980 - Section 1.2.1 if (m == "STREAM") { line = "203 Streaming is OK\r\n"; send "writeBytes"(line) to connObj from nil; return (0); } line = makeAsString("500 MODE Command ", m, " not understood\r\n"); send "writeBytes"(line) to connObj from nil; return (0); } NNTPclientConnection:_authInfo(array tokens) { any m, arg; string line; m = convertCase(tokens[1], 0); // RFC 2980 - Section 3.1 if (m == "USER") { userName = tokens[2]; // userName // if user name no match, send 482 - authentication rejected line = "381 More authentication data required\r\n"; send "writeBytes"(line) to connObj from nil; return (0); } if (m == "PASS") { if (typeOf(userName) != string) { line = "482 Authentication rejected\r\n"; send "writeBytes"(line) to connObj from nil; return (0); } arg = tokens[2]; // password // no match, send 482 - authentication rejected line = "281 Authentication accepted\r\n"; send "writeBytes"(line) to connObj from nil; return (0); } // TO DO: SIMPLE // TO DO: GENERIC line = makeAsString("500 AUTHINFO Command ", m, " not understood\r\n"); send "writeBytes"(line) to connObj from nil; return (0); } NNTPclientConnection:_xover(array tokens) { set lines; any artInfo, arg, fmtLines, groupInfo, range; oid obj; int i, first, last; if (currentNewsgroupObj == nil) { // not selected send "writeBytes"("412 No newsgroup has yet been selected\r\n") to connObj from nil; return (0); } if (indexExists(tokens, 1) == 0) { // use current article if (currentArticleInfo == nil) { send "writeBytes"("420 No article has yet been selected\r\n") to connObj from nil; return (0); } lines += "224 Response for XOVER follows\r\n"; fmtLines = call "_fmtXOVER"(99999, currentArticleInfo[0]); lines |= fmtLines; lines += ".\r\n"; send "writeVectorOfBytes"(0, lines) to connObj from nil; return (0); } arg = tokens[1]; if (findSubstring(arg, "-") == -1) { // no range i = stringToNumber(arg, int); artInfo = send "findArticleNumber"(i) to currentNewsgroupObj; if (artInfo == nil) { send "writeBytes"("420 No such article\r\n") to connObj from nil; return (0); } obj = artInfo[0]; lines += "224 Response for XOVER follows\r\n"; fmtLines = call "_fmtXOVER"(i, obj); lines += ".\r\n"; send "writeVectorOfBytes"(0, lines) to connObj from nil; return (0); } groupInfo = send "getNewsgroupInfo" to currentNewsgroupObj; range = tokenizeString(arg, "-", 1); if (midchar(arg, 0) == '-') { first = groupInfo["firstArticle"]; last = range[0]; } else { first = range[0]; if (indexExists(range, 1) != 0) last = range[1]; else last = groupInfo["lastArticle"]; } lines += "224 Response for XOVER follows\r\n"; for(i=first;i<=last;i+=1) { artInfo = send "findArticleNumber"(i) to currentNewsgroupObj; if (artInfo != nil) { obj = artInfo[0]; fmtLines = call "_fmtXOVER"(i, obj); lines |= fmtLines; } } lines += ".\r\n"; send "writeVectorOfBytes"(0, lines) to connObj from nil; return (0); } NNTPclientConnection:_fmtXOVER(int artNum, oid obj) { any info; set elements; string line, body, separator; int l, count, offset; info = send "getHeaderData" to obj; body = send "getArticleBody" to obj; // articleNum, suject, author, date, message-id, references, byte-count, line-count separator = "\t"; elements += makeAsString(artNum, separator, info["subject"], separator, info["from"], separator, info["date"], separator, info["message-id"], separator); if (indexExists(info, "references") != 0) { elements += info["references"]; } elements += separator; // byte-count l = length(body); elements += makeAsString(l); elements += separator; // line-count offset = 0; count = 0; while (offset < l) { offset = findSubstringAfter(body, "\n", offset); if (offset == -1) break; count += 1; offset += 1; // skip newline } line = makeAsString(elements, count, "\r\n"); return (line); } NNTPclientConnection:systemShutdown() { string line; int rc; line = makeAsString("400 ", serverConfig["hostName"], " is shutting down\r\n"); rc = send "writeBytes"(line) to connObj; send "deleteYourself" to readBfr; readBfr = nil; sessionClosed = 1; shutdownOID = nil; }