blob: 5a16fcbf64c8701a9ea42f8037262a75d9f62836 [file] [log] [blame]
Guido van Rossum54f22ed2000-02-04 15:10:34 +00001"""An NNTP client class based on RFC 977: Network News Transfer Protocol.
Guido van Rossumc629d341992-11-05 10:43:02 +00002
Guido van Rossum54f22ed2000-02-04 15:10:34 +00003Example:
Guido van Rossumc629d341992-11-05 10:43:02 +00004
Guido van Rossum54f22ed2000-02-04 15:10:34 +00005>>> from nntplib import NNTP
6>>> s = NNTP('news')
7>>> resp, count, first, last, name = s.group('comp.lang.python')
8>>> print 'Group', name, 'has', count, 'articles, range', first, 'to', last
9Group comp.lang.python has 51 articles, range 5770 to 5821
10>>> resp, subs = s.xhdr('subject', first + '-' + last)
11>>> resp = s.quit()
12>>>
Guido van Rossumc629d341992-11-05 10:43:02 +000013
Guido van Rossum54f22ed2000-02-04 15:10:34 +000014Here 'resp' is the server response line.
15Error responses are turned into exceptions.
16
17To post an article from a file:
18>>> f = open(filename, 'r') # file containing article, including header
19>>> resp = s.post(f)
20>>>
21
22For descriptions of all methods, read the comments in the code below.
23Note that all arguments and return values representing article numbers
24are strings, not numbers, since they are rarely used for calculations.
25"""
26
27# RFC 977 by Brian Kantor and Phil Lapsley.
28# xover, xgtitle, xpath, date methods by Kevan Heydon
Guido van Rossum8421c4e1995-09-22 00:52:38 +000029
Guido van Rossumc629d341992-11-05 10:43:02 +000030
31# Imports
Guido van Rossum9694fca1997-10-22 21:00:49 +000032import re
Guido van Rossumc629d341992-11-05 10:43:02 +000033import socket
34import string
35
36
Guido van Rossum18fc5691992-11-26 09:17:19 +000037# Exception raised when an error or invalid response is received
Guido van Rossum18fc5691992-11-26 09:17:19 +000038error_reply = 'nntplib.error_reply' # unexpected [123]xx reply
39error_temp = 'nntplib.error_temp' # 4xx errors
40error_perm = 'nntplib.error_perm' # 5xx errors
41error_proto = 'nntplib.error_proto' # response does not begin with [1-5]
Guido van Rossum8421c4e1995-09-22 00:52:38 +000042error_data = 'nntplib.error_data' # error in response data
Guido van Rossumc629d341992-11-05 10:43:02 +000043
44
45# Standard port used by NNTP servers
46NNTP_PORT = 119
47
48
49# Response numbers that are followed by additional text (e.g. article)
Guido van Rossum8421c4e1995-09-22 00:52:38 +000050LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
Guido van Rossumc629d341992-11-05 10:43:02 +000051
52
53# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
54CRLF = '\r\n'
55
56
57# The class itself
58
59class NNTP:
60
Guido van Rossumdd659751997-10-20 23:29:44 +000061 def __init__(self, host, port = NNTP_PORT, user=None, password=None):
Guido van Rossum54f22ed2000-02-04 15:10:34 +000062 """Initialize an instance. Arguments:
63 - host: hostname to connect to
64 - port: port to connect to (default the standard NNTP port)"""
65
Guido van Rossumc629d341992-11-05 10:43:02 +000066 self.host = host
67 self.port = port
68 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
69 self.sock.connect(self.host, self.port)
Jack Jansen2bb57b81996-02-14 16:06:24 +000070 self.file = self.sock.makefile('rb')
Guido van Rossumc629d341992-11-05 10:43:02 +000071 self.debugging = 0
72 self.welcome = self.getresp()
Guido van Rossumdd659751997-10-20 23:29:44 +000073 if user:
Guido van Rossum8ca84201998-03-26 20:56:10 +000074 resp = self.shortcmd('authinfo user '+user)
75 if resp[:3] == '381':
76 if not password:
77 raise error_reply, resp
78 else:
79 resp = self.shortcmd(
80 'authinfo pass '+password)
81 if resp[:3] != '281':
82 raise error_perm, resp
Guido van Rossumc629d341992-11-05 10:43:02 +000083
Guido van Rossumc629d341992-11-05 10:43:02 +000084 def getwelcome(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +000085 """Get the welcome message from the server
86 (this is read and squirreled away by __init__()).
87 If the response code is 200, posting is allowed;
88 if it 201, posting is not allowed."""
89
Guido van Rossumc629d341992-11-05 10:43:02 +000090 if self.debugging: print '*welcome*', `self.welcome`
91 return self.welcome
92
Guido van Rossumcf5394f1995-03-30 10:42:34 +000093 def set_debuglevel(self, level):
Guido van Rossum54f22ed2000-02-04 15:10:34 +000094 """Set the debugging level. Argument 'level' means:
95 0: no debugging output (default)
96 1: print commands and responses but not body text etc.
97 2: also print raw lines read and sent before stripping CR/LF"""
98
Guido van Rossumc629d341992-11-05 10:43:02 +000099 self.debugging = level
Guido van Rossumcf5394f1995-03-30 10:42:34 +0000100 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000101
Guido van Rossumc629d341992-11-05 10:43:02 +0000102 def putline(self, line):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000103 """Internal: send one line to the server, appending CRLF."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000104 line = line + CRLF
105 if self.debugging > 1: print '*put*', `line`
106 self.sock.send(line)
107
Guido van Rossumc629d341992-11-05 10:43:02 +0000108 def putcmd(self, line):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000109 """Internal: send one command to the server (through putline())."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000110 if self.debugging: print '*cmd*', `line`
111 self.putline(line)
112
Guido van Rossumc629d341992-11-05 10:43:02 +0000113 def getline(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000114 """Internal: return one line from the server, stripping CRLF.
115 Raise EOFError if the connection is closed."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000116 line = self.file.readline()
117 if self.debugging > 1:
118 print '*get*', `line`
119 if not line: raise EOFError
120 if line[-2:] == CRLF: line = line[:-2]
121 elif line[-1:] in CRLF: line = line[:-1]
122 return line
123
Guido van Rossumc629d341992-11-05 10:43:02 +0000124 def getresp(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000125 """Internal: get a response from the server.
126 Raise various errors if the response indicates an error."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000127 resp = self.getline()
128 if self.debugging: print '*resp*', `resp`
129 c = resp[:1]
130 if c == '4':
Guido van Rossum18fc5691992-11-26 09:17:19 +0000131 raise error_temp, resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000132 if c == '5':
Guido van Rossum18fc5691992-11-26 09:17:19 +0000133 raise error_perm, resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000134 if c not in '123':
Guido van Rossum18fc5691992-11-26 09:17:19 +0000135 raise error_proto, resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000136 return resp
137
Guido van Rossumc629d341992-11-05 10:43:02 +0000138 def getlongresp(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000139 """Internal: get a response plus following text from the server.
140 Raise various errors if the response indicates an error."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000141 resp = self.getresp()
142 if resp[:3] not in LONGRESP:
143 raise error_reply, resp
144 list = []
145 while 1:
146 line = self.getline()
147 if line == '.':
148 break
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000149 if line[:2] == '..':
150 line = line[1:]
Guido van Rossumc629d341992-11-05 10:43:02 +0000151 list.append(line)
152 return resp, list
153
Guido van Rossumc629d341992-11-05 10:43:02 +0000154 def shortcmd(self, line):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000155 """Internal: send a command and get the response."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000156 self.putcmd(line)
157 return self.getresp()
158
Guido van Rossumc629d341992-11-05 10:43:02 +0000159 def longcmd(self, line):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000160 """Internal: send a command and get the response plus following text."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000161 self.putcmd(line)
162 return self.getlongresp()
163
Guido van Rossumc629d341992-11-05 10:43:02 +0000164 def newgroups(self, date, time):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000165 """Process a NEWGROUPS command. Arguments:
166 - date: string 'yymmdd' indicating the date
167 - time: string 'hhmmss' indicating the time
168 Return:
169 - resp: server response if succesful
170 - list: list of newsgroup names"""
171
Guido van Rossumc629d341992-11-05 10:43:02 +0000172 return self.longcmd('NEWGROUPS ' + date + ' ' + time)
173
Guido van Rossumc629d341992-11-05 10:43:02 +0000174 def newnews(self, group, date, time):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000175 """Process a NEWNEWS command. Arguments:
176 - group: group name or '*'
177 - date: string 'yymmdd' indicating the date
178 - time: string 'hhmmss' indicating the time
179 Return:
180 - resp: server response if succesful
181 - list: list of article ids"""
182
Guido van Rossumc629d341992-11-05 10:43:02 +0000183 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
184 return self.longcmd(cmd)
185
Guido van Rossumc629d341992-11-05 10:43:02 +0000186 def list(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000187 """Process a LIST command. Return:
188 - resp: server response if succesful
189 - list: list of (group, last, first, flag) (strings)"""
190
Guido van Rossumc629d341992-11-05 10:43:02 +0000191 resp, list = self.longcmd('LIST')
192 for i in range(len(list)):
Guido van Rossumbe9f2121995-01-10 10:35:55 +0000193 # Parse lines into "group last first flag"
Guido van Rossumc69955341997-03-14 04:18:20 +0000194 list[i] = tuple(string.split(list[i]))
Guido van Rossumc629d341992-11-05 10:43:02 +0000195 return resp, list
196
Guido van Rossumc629d341992-11-05 10:43:02 +0000197 def group(self, name):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000198 """Process a GROUP command. Argument:
199 - group: the group name
200 Returns:
201 - resp: server response if succesful
202 - count: number of articles (string)
203 - first: first article number (string)
204 - last: last article number (string)
205 - name: the group name"""
206
Guido van Rossumc629d341992-11-05 10:43:02 +0000207 resp = self.shortcmd('GROUP ' + name)
208 if resp[:3] <> '211':
209 raise error_reply, resp
210 words = string.split(resp)
211 count = first = last = 0
212 n = len(words)
213 if n > 1:
214 count = words[1]
215 if n > 2:
216 first = words[2]
217 if n > 3:
218 last = words[3]
219 if n > 4:
220 name = string.lower(words[4])
221 return resp, count, first, last, name
222
Guido van Rossumc629d341992-11-05 10:43:02 +0000223 def help(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000224 """Process a HELP command. Returns:
225 - resp: server response if succesful
226 - list: list of strings"""
227
Guido van Rossumc629d341992-11-05 10:43:02 +0000228 return self.longcmd('HELP')
229
Guido van Rossumc629d341992-11-05 10:43:02 +0000230 def statparse(self, resp):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000231 """Internal: parse the response of a STAT, NEXT or LAST command."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000232 if resp[:2] <> '22':
233 raise error_reply, resp
234 words = string.split(resp)
235 nr = 0
236 id = ''
237 n = len(words)
238 if n > 1:
239 nr = words[1]
240 if n > 2:
Guido van Rossum98c17b31998-12-21 18:51:23 +0000241 id = words[2]
Guido van Rossumc629d341992-11-05 10:43:02 +0000242 return resp, nr, id
243
Guido van Rossumc629d341992-11-05 10:43:02 +0000244 def statcmd(self, line):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000245 """Internal: process a STAT, NEXT or LAST command."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000246 resp = self.shortcmd(line)
247 return self.statparse(resp)
248
Guido van Rossumc629d341992-11-05 10:43:02 +0000249 def stat(self, id):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000250 """Process a STAT command. Argument:
251 - id: article number or message id
252 Returns:
253 - resp: server response if succesful
254 - nr: the article number
255 - id: the article id"""
256
Guido van Rossumc629d341992-11-05 10:43:02 +0000257 return self.statcmd('STAT ' + id)
258
Guido van Rossumc629d341992-11-05 10:43:02 +0000259 def next(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000260 """Process a NEXT command. No arguments. Return as for STAT."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000261 return self.statcmd('NEXT')
262
Guido van Rossumc629d341992-11-05 10:43:02 +0000263 def last(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000264 """Process a LAST command. No arguments. Return as for STAT."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000265 return self.statcmd('LAST')
266
Guido van Rossumc629d341992-11-05 10:43:02 +0000267 def artcmd(self, line):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000268 """Internal: process a HEAD, BODY or ARTICLE command."""
Guido van Rossumc629d341992-11-05 10:43:02 +0000269 resp, list = self.longcmd(line)
270 resp, nr, id = self.statparse(resp)
271 return resp, nr, id, list
272
Guido van Rossumc629d341992-11-05 10:43:02 +0000273 def head(self, id):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000274 """Process a HEAD command. Argument:
275 - id: article number or message id
276 Returns:
277 - resp: server response if succesful
278 - nr: article number
279 - id: message id
280 - list: the lines of the article's header"""
281
Guido van Rossumc629d341992-11-05 10:43:02 +0000282 return self.artcmd('HEAD ' + id)
283
Guido van Rossumc629d341992-11-05 10:43:02 +0000284 def body(self, id):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000285 """Process a BODY command. Argument:
286 - id: article number or message id
287 Returns:
288 - resp: server response if succesful
289 - nr: article number
290 - id: message id
291 - list: the lines of the article's body"""
292
Guido van Rossumc629d341992-11-05 10:43:02 +0000293 return self.artcmd('BODY ' + id)
294
Guido van Rossumc629d341992-11-05 10:43:02 +0000295 def article(self, id):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000296 """Process an ARTICLE command. Argument:
297 - id: article number or message id
298 Returns:
299 - resp: server response if succesful
300 - nr: article number
301 - id: message id
302 - list: the lines of the article"""
303
Guido van Rossumc629d341992-11-05 10:43:02 +0000304 return self.artcmd('ARTICLE ' + id)
305
Guido van Rossumc629d341992-11-05 10:43:02 +0000306 def slave(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000307 """Process a SLAVE command. Returns:
308 - resp: server response if succesful"""
309
Guido van Rossumc629d341992-11-05 10:43:02 +0000310 return self.shortcmd('SLAVE')
311
Guido van Rossumc629d341992-11-05 10:43:02 +0000312 def xhdr(self, hdr, str):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000313 """Process an XHDR command (optional server extension). Arguments:
314 - hdr: the header type (e.g. 'subject')
315 - str: an article nr, a message id, or a range nr1-nr2
316 Returns:
317 - resp: server response if succesful
318 - list: list of (nr, value) strings"""
319
Guido van Rossum9694fca1997-10-22 21:00:49 +0000320 pat = re.compile('^([0-9]+) ?(.*)\n?')
Guido van Rossumc629d341992-11-05 10:43:02 +0000321 resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str)
322 for i in range(len(lines)):
323 line = lines[i]
Guido van Rossum9694fca1997-10-22 21:00:49 +0000324 m = pat.match(line)
325 if m:
326 lines[i] = m.group(1, 2)
Guido van Rossumc629d341992-11-05 10:43:02 +0000327 return resp, lines
328
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000329 def xover(self,start,end):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000330 """Process an XOVER command (optional server extension) Arguments:
331 - start: start of range
332 - end: end of range
333 Returns:
334 - resp: server response if succesful
335 - list: list of (art-nr, subject, poster, date,
336 id, references, size, lines)"""
337
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000338 resp, lines = self.longcmd('XOVER ' + start + '-' + end)
339 xover_lines = []
340 for line in lines:
341 elem = string.splitfields(line,"\t")
342 try:
Guido van Rossumc3fb88b1997-07-17 15:21:52 +0000343 xover_lines.append((elem[0],
344 elem[1],
345 elem[2],
346 elem[3],
347 elem[4],
348 string.split(elem[5]),
349 elem[6],
350 elem[7]))
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000351 except IndexError:
352 raise error_data,line
353 return resp,xover_lines
354
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000355 def xgtitle(self, group):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000356 """Process an XGTITLE command (optional server extension) Arguments:
357 - group: group name wildcard (i.e. news.*)
358 Returns:
359 - resp: server response if succesful
360 - list: list of (name,title) strings"""
361
Guido van Rossum9694fca1997-10-22 21:00:49 +0000362 line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000363 resp, raw_lines = self.longcmd('XGTITLE ' + group)
364 lines = []
365 for raw_line in raw_lines:
Guido van Rossum9694fca1997-10-22 21:00:49 +0000366 match = line_pat.search(string.strip(raw_line))
367 if match:
368 lines.append(match.group(1, 2))
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000369 return resp, lines
370
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000371 def xpath(self,id):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000372 """Process an XPATH command (optional server extension) Arguments:
373 - id: Message id of article
374 Returns:
375 resp: server response if succesful
376 path: directory path to article"""
377
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000378 resp = self.shortcmd("XPATH " + id)
379 if resp[:3] <> '223':
380 raise error_reply, resp
381 try:
382 [resp_num, path] = string.split(resp)
383 except ValueError:
384 raise error_reply, resp
385 else:
386 return resp, path
387
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000388 def date (self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000389 """Process the DATE command. Arguments:
390 None
391 Returns:
392 resp: server response if succesful
393 date: Date suitable for newnews/newgroups commands etc.
394 time: Time suitable for newnews/newgroups commands etc."""
395
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000396 resp = self.shortcmd("DATE")
397 if resp[:3] <> '111':
398 raise error_reply, resp
399 elem = string.split(resp)
400 if len(elem) != 2:
401 raise error_data, resp
402 date = elem[1][2:8]
403 time = elem[1][-6:]
404 if len(date) != 6 or len(time) != 6:
405 raise error_data, resp
406 return resp, date, time
407
408
Guido van Rossumc629d341992-11-05 10:43:02 +0000409 def post(self, f):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000410 """Process a POST command. Arguments:
411 - f: file containing the article
412 Returns:
413 - resp: server response if succesful"""
414
Guido van Rossumc629d341992-11-05 10:43:02 +0000415 resp = self.shortcmd('POST')
416 # Raises error_??? if posting is not allowed
417 if resp[0] <> '3':
418 raise error_reply, resp
419 while 1:
420 line = f.readline()
421 if not line:
422 break
423 if line[-1] == '\n':
424 line = line[:-1]
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000425 if line[:1] == '.':
426 line = '.' + line
Guido van Rossumc629d341992-11-05 10:43:02 +0000427 self.putline(line)
428 self.putline('.')
429 return self.getresp()
430
Guido van Rossumc629d341992-11-05 10:43:02 +0000431 def ihave(self, id, f):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000432 """Process an IHAVE command. Arguments:
433 - id: message-id of the article
434 - f: file containing the article
435 Returns:
436 - resp: server response if succesful
437 Note that if the server refuses the article an exception is raised."""
438
Guido van Rossumc629d341992-11-05 10:43:02 +0000439 resp = self.shortcmd('IHAVE ' + id)
Guido van Rossum18fc5691992-11-26 09:17:19 +0000440 # Raises error_??? if the server already has it
Guido van Rossumc629d341992-11-05 10:43:02 +0000441 if resp[0] <> '3':
442 raise error_reply, resp
443 while 1:
444 line = f.readline()
445 if not line:
446 break
447 if line[-1] == '\n':
448 line = line[:-1]
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000449 if line[:1] == '.':
450 line = '.' + line
Guido van Rossumc629d341992-11-05 10:43:02 +0000451 self.putline(line)
452 self.putline('.')
453 return self.getresp()
454
Guido van Rossumc629d341992-11-05 10:43:02 +0000455 def quit(self):
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000456 """Process a QUIT command and close the socket. Returns:
457 - resp: server response if succesful"""
458
Guido van Rossumc629d341992-11-05 10:43:02 +0000459 resp = self.shortcmd('QUIT')
460 self.file.close()
461 self.sock.close()
462 del self.file, self.sock
463 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000464
465
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000466def _test():
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000467 """Minimal test function."""
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000468 s = NNTP('news')
469 resp, count, first, last, name = s.group('comp.lang.python')
470 print resp
471 print 'Group', name, 'has', count, 'articles, range', first, 'to', last
472 resp, subs = s.xhdr('subject', first + '-' + last)
473 print resp
474 for item in subs:
475 print "%7s %s" % item
476 resp = s.quit()
477 print resp
478
479
480# Run the test when run as a script
481if __name__ == '__main__':
482 _test()