This commit was manufactured by cvs2svn to create tag 'r222'.
[python/dscho.git] / Lib / nntplib.py
blob21faab1aa36aa16370a1ce0cf024dcf7a0d968fe
1 """An NNTP client class based on RFC 977: Network News Transfer Protocol.
3 Example:
5 >>> 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
9 Group comp.lang.python has 51 articles, range 5770 to 5821
10 >>> resp, subs = s.xhdr('subject', first + '-' + last)
11 >>> resp = s.quit()
12 >>>
14 Here 'resp' is the server response line.
15 Error responses are turned into exceptions.
17 To post an article from a file:
18 >>> f = open(filename, 'r') # file containing article, including header
19 >>> resp = s.post(f)
20 >>>
22 For descriptions of all methods, read the comments in the code below.
23 Note that all arguments and return values representing article numbers
24 are strings, not numbers, since they are rarely used for calculations.
25 """
27 # RFC 977 by Brian Kantor and Phil Lapsley.
28 # xover, xgtitle, xpath, date methods by Kevan Heydon
31 # Imports
32 import re
33 import socket
34 import types
36 __all__ = ["NNTP","NNTPReplyError","NNTPTemporaryError",
37 "NNTPPermanentError","NNTPProtocolError","NNTPDataError",
38 "error_reply","error_temp","error_perm","error_proto",
39 "error_data",]
41 # Exceptions raised when an error or invalid response is received
42 class NNTPError(Exception):
43 """Base class for all nntplib exceptions"""
44 def __init__(self, *args):
45 apply(Exception.__init__, (self,)+args)
46 try:
47 self.response = args[0]
48 except IndexError:
49 self.response = 'No response given'
51 class NNTPReplyError(NNTPError):
52 """Unexpected [123]xx reply"""
53 pass
55 class NNTPTemporaryError(NNTPError):
56 """4xx errors"""
57 pass
59 class NNTPPermanentError(NNTPError):
60 """5xx errors"""
61 pass
63 class NNTPProtocolError(NNTPError):
64 """Response does not begin with [1-5]"""
65 pass
67 class NNTPDataError(NNTPError):
68 """Error in response data"""
69 pass
71 # for backwards compatibility
72 error_reply = NNTPReplyError
73 error_temp = NNTPTemporaryError
74 error_perm = NNTPPermanentError
75 error_proto = NNTPProtocolError
76 error_data = NNTPDataError
80 # Standard port used by NNTP servers
81 NNTP_PORT = 119
84 # Response numbers that are followed by additional text (e.g. article)
85 LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
88 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
89 CRLF = '\r\n'
93 # The class itself
94 class NNTP:
95 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
96 readermode=None):
97 """Initialize an instance. Arguments:
98 - host: hostname to connect to
99 - port: port to connect to (default the standard NNTP port)
100 - user: username to authenticate with
101 - password: password to use with username
102 - readermode: if true, send 'mode reader' command after
103 connecting.
105 readermode is sometimes necessary if you are connecting to an
106 NNTP server on the local machine and intend to call
107 reader-specific comamnds, such as `group'. If you get
108 unexpected NNTPPermanentErrors, you might need to set
109 readermode.
111 self.host = host
112 self.port = port
113 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
114 self.sock.connect((self.host, self.port))
115 self.file = self.sock.makefile('rb')
116 self.debugging = 0
117 self.welcome = self.getresp()
119 # 'mode reader' is sometimes necessary to enable 'reader' mode.
120 # However, the order in which 'mode reader' and 'authinfo' need to
121 # arrive differs between some NNTP servers. Try to send
122 # 'mode reader', and if it fails with an authorization failed
123 # error, try again after sending authinfo.
124 readermode_afterauth = 0
125 if readermode:
126 try:
127 self.welcome = self.shortcmd('mode reader')
128 except NNTPPermanentError:
129 # error 500, probably 'not implemented'
130 pass
131 except NNTPTemporaryError, e:
132 if user and e.response[:3] == '480':
133 # Need authorization before 'mode reader'
134 readermode_afterauth = 1
135 else:
136 raise
137 if user:
138 resp = self.shortcmd('authinfo user '+user)
139 if resp[:3] == '381':
140 if not password:
141 raise NNTPReplyError(resp)
142 else:
143 resp = self.shortcmd(
144 'authinfo pass '+password)
145 if resp[:3] != '281':
146 raise NNTPPermanentError(resp)
147 if readermode_afterauth:
148 try:
149 self.welcome = self.shortcmd('mode reader')
150 except NNTPPermanentError:
151 # error 500, probably 'not implemented'
152 pass
155 # Get the welcome message from the server
156 # (this is read and squirreled away by __init__()).
157 # If the response code is 200, posting is allowed;
158 # if it 201, posting is not allowed
160 def getwelcome(self):
161 """Get the welcome message from the server
162 (this is read and squirreled away by __init__()).
163 If the response code is 200, posting is allowed;
164 if it 201, posting is not allowed."""
166 if self.debugging: print '*welcome*', `self.welcome`
167 return self.welcome
169 def set_debuglevel(self, level):
170 """Set the debugging level. Argument 'level' means:
171 0: no debugging output (default)
172 1: print commands and responses but not body text etc.
173 2: also print raw lines read and sent before stripping CR/LF"""
175 self.debugging = level
176 debug = set_debuglevel
178 def putline(self, line):
179 """Internal: send one line to the server, appending CRLF."""
180 line = line + CRLF
181 if self.debugging > 1: print '*put*', `line`
182 self.sock.sendall(line)
184 def putcmd(self, line):
185 """Internal: send one command to the server (through putline())."""
186 if self.debugging: print '*cmd*', `line`
187 self.putline(line)
189 def getline(self):
190 """Internal: return one line from the server, stripping CRLF.
191 Raise EOFError if the connection is closed."""
192 line = self.file.readline()
193 if self.debugging > 1:
194 print '*get*', `line`
195 if not line: raise EOFError
196 if line[-2:] == CRLF: line = line[:-2]
197 elif line[-1:] in CRLF: line = line[:-1]
198 return line
200 def getresp(self):
201 """Internal: get a response from the server.
202 Raise various errors if the response indicates an error."""
203 resp = self.getline()
204 if self.debugging: print '*resp*', `resp`
205 c = resp[:1]
206 if c == '4':
207 raise NNTPTemporaryError(resp)
208 if c == '5':
209 raise NNTPPermanentError(resp)
210 if c not in '123':
211 raise NNTPProtocolError(resp)
212 return resp
214 def getlongresp(self, file=None):
215 """Internal: get a response plus following text from the server.
216 Raise various errors if the response indicates an error."""
218 openedFile = None
219 try:
220 # If a string was passed then open a file with that name
221 if isinstance(file, types.StringType):
222 openedFile = file = open(file, "w")
224 resp = self.getresp()
225 if resp[:3] not in LONGRESP:
226 raise NNTPReplyError(resp)
227 list = []
228 while 1:
229 line = self.getline()
230 if line == '.':
231 break
232 if line[:2] == '..':
233 line = line[1:]
234 if file:
235 file.write(line + "\n")
236 else:
237 list.append(line)
238 finally:
239 # If this method created the file, then it must close it
240 if openedFile:
241 openedFile.close()
243 return resp, list
245 def shortcmd(self, line):
246 """Internal: send a command and get the response."""
247 self.putcmd(line)
248 return self.getresp()
250 def longcmd(self, line, file=None):
251 """Internal: send a command and get the response plus following text."""
252 self.putcmd(line)
253 return self.getlongresp(file)
255 def newgroups(self, date, time):
256 """Process a NEWGROUPS command. Arguments:
257 - date: string 'yymmdd' indicating the date
258 - time: string 'hhmmss' indicating the time
259 Return:
260 - resp: server response if successful
261 - list: list of newsgroup names"""
263 return self.longcmd('NEWGROUPS ' + date + ' ' + time)
265 def newnews(self, group, date, time):
266 """Process a NEWNEWS command. Arguments:
267 - group: group name or '*'
268 - date: string 'yymmdd' indicating the date
269 - time: string 'hhmmss' indicating the time
270 Return:
271 - resp: server response if successful
272 - list: list of article ids"""
274 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
275 return self.longcmd(cmd)
277 def list(self):
278 """Process a LIST command. Return:
279 - resp: server response if successful
280 - list: list of (group, last, first, flag) (strings)"""
282 resp, list = self.longcmd('LIST')
283 for i in range(len(list)):
284 # Parse lines into "group last first flag"
285 list[i] = tuple(list[i].split())
286 return resp, list
288 def group(self, name):
289 """Process a GROUP command. Argument:
290 - group: the group name
291 Returns:
292 - resp: server response if successful
293 - count: number of articles (string)
294 - first: first article number (string)
295 - last: last article number (string)
296 - name: the group name"""
298 resp = self.shortcmd('GROUP ' + name)
299 if resp[:3] != '211':
300 raise NNTPReplyError(resp)
301 words = resp.split()
302 count = first = last = 0
303 n = len(words)
304 if n > 1:
305 count = words[1]
306 if n > 2:
307 first = words[2]
308 if n > 3:
309 last = words[3]
310 if n > 4:
311 name = words[4].lower()
312 return resp, count, first, last, name
314 def help(self):
315 """Process a HELP command. Returns:
316 - resp: server response if successful
317 - list: list of strings"""
319 return self.longcmd('HELP')
321 def statparse(self, resp):
322 """Internal: parse the response of a STAT, NEXT or LAST command."""
323 if resp[:2] != '22':
324 raise NNTPReplyError(resp)
325 words = resp.split()
326 nr = 0
327 id = ''
328 n = len(words)
329 if n > 1:
330 nr = words[1]
331 if n > 2:
332 id = words[2]
333 return resp, nr, id
335 def statcmd(self, line):
336 """Internal: process a STAT, NEXT or LAST command."""
337 resp = self.shortcmd(line)
338 return self.statparse(resp)
340 def stat(self, id):
341 """Process a STAT command. Argument:
342 - id: article number or message id
343 Returns:
344 - resp: server response if successful
345 - nr: the article number
346 - id: the article id"""
348 return self.statcmd('STAT ' + id)
350 def next(self):
351 """Process a NEXT command. No arguments. Return as for STAT."""
352 return self.statcmd('NEXT')
354 def last(self):
355 """Process a LAST command. No arguments. Return as for STAT."""
356 return self.statcmd('LAST')
358 def artcmd(self, line, file=None):
359 """Internal: process a HEAD, BODY or ARTICLE command."""
360 resp, list = self.longcmd(line, file)
361 resp, nr, id = self.statparse(resp)
362 return resp, nr, id, list
364 def head(self, id):
365 """Process a HEAD command. Argument:
366 - id: article number or message id
367 Returns:
368 - resp: server response if successful
369 - nr: article number
370 - id: message id
371 - list: the lines of the article's header"""
373 return self.artcmd('HEAD ' + id)
375 def body(self, id, file=None):
376 """Process a BODY command. Argument:
377 - id: article number or message id
378 - file: Filename string or file object to store the article in
379 Returns:
380 - resp: server response if successful
381 - nr: article number
382 - id: message id
383 - list: the lines of the article's body or an empty list
384 if file was used"""
386 return self.artcmd('BODY ' + id, file)
388 def article(self, id):
389 """Process an ARTICLE command. Argument:
390 - id: article number or message id
391 Returns:
392 - resp: server response if successful
393 - nr: article number
394 - id: message id
395 - list: the lines of the article"""
397 return self.artcmd('ARTICLE ' + id)
399 def slave(self):
400 """Process a SLAVE command. Returns:
401 - resp: server response if successful"""
403 return self.shortcmd('SLAVE')
405 def xhdr(self, hdr, str):
406 """Process an XHDR command (optional server extension). Arguments:
407 - hdr: the header type (e.g. 'subject')
408 - str: an article nr, a message id, or a range nr1-nr2
409 Returns:
410 - resp: server response if successful
411 - list: list of (nr, value) strings"""
413 pat = re.compile('^([0-9]+) ?(.*)\n?')
414 resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str)
415 for i in range(len(lines)):
416 line = lines[i]
417 m = pat.match(line)
418 if m:
419 lines[i] = m.group(1, 2)
420 return resp, lines
422 def xover(self,start,end):
423 """Process an XOVER command (optional server extension) Arguments:
424 - start: start of range
425 - end: end of range
426 Returns:
427 - resp: server response if successful
428 - list: list of (art-nr, subject, poster, date,
429 id, references, size, lines)"""
431 resp, lines = self.longcmd('XOVER ' + start + '-' + end)
432 xover_lines = []
433 for line in lines:
434 elem = line.split("\t")
435 try:
436 xover_lines.append((elem[0],
437 elem[1],
438 elem[2],
439 elem[3],
440 elem[4],
441 elem[5].split(),
442 elem[6],
443 elem[7]))
444 except IndexError:
445 raise NNTPDataError(line)
446 return resp,xover_lines
448 def xgtitle(self, group):
449 """Process an XGTITLE command (optional server extension) Arguments:
450 - group: group name wildcard (i.e. news.*)
451 Returns:
452 - resp: server response if successful
453 - list: list of (name,title) strings"""
455 line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
456 resp, raw_lines = self.longcmd('XGTITLE ' + group)
457 lines = []
458 for raw_line in raw_lines:
459 match = line_pat.search(raw_line.strip())
460 if match:
461 lines.append(match.group(1, 2))
462 return resp, lines
464 def xpath(self,id):
465 """Process an XPATH command (optional server extension) Arguments:
466 - id: Message id of article
467 Returns:
468 resp: server response if successful
469 path: directory path to article"""
471 resp = self.shortcmd("XPATH " + id)
472 if resp[:3] != '223':
473 raise NNTPReplyError(resp)
474 try:
475 [resp_num, path] = resp.split()
476 except ValueError:
477 raise NNTPReplyError(resp)
478 else:
479 return resp, path
481 def date (self):
482 """Process the DATE command. Arguments:
483 None
484 Returns:
485 resp: server response if successful
486 date: Date suitable for newnews/newgroups commands etc.
487 time: Time suitable for newnews/newgroups commands etc."""
489 resp = self.shortcmd("DATE")
490 if resp[:3] != '111':
491 raise NNTPReplyError(resp)
492 elem = resp.split()
493 if len(elem) != 2:
494 raise NNTPDataError(resp)
495 date = elem[1][2:8]
496 time = elem[1][-6:]
497 if len(date) != 6 or len(time) != 6:
498 raise NNTPDataError(resp)
499 return resp, date, time
502 def post(self, f):
503 """Process a POST command. Arguments:
504 - f: file containing the article
505 Returns:
506 - resp: server response if successful"""
508 resp = self.shortcmd('POST')
509 # Raises error_??? if posting is not allowed
510 if resp[0] != '3':
511 raise NNTPReplyError(resp)
512 while 1:
513 line = f.readline()
514 if not line:
515 break
516 if line[-1] == '\n':
517 line = line[:-1]
518 if line[:1] == '.':
519 line = '.' + line
520 self.putline(line)
521 self.putline('.')
522 return self.getresp()
524 def ihave(self, id, f):
525 """Process an IHAVE command. Arguments:
526 - id: message-id of the article
527 - f: file containing the article
528 Returns:
529 - resp: server response if successful
530 Note that if the server refuses the article an exception is raised."""
532 resp = self.shortcmd('IHAVE ' + id)
533 # Raises error_??? if the server already has it
534 if resp[0] != '3':
535 raise NNTPReplyError(resp)
536 while 1:
537 line = f.readline()
538 if not line:
539 break
540 if line[-1] == '\n':
541 line = line[:-1]
542 if line[:1] == '.':
543 line = '.' + line
544 self.putline(line)
545 self.putline('.')
546 return self.getresp()
548 def quit(self):
549 """Process a QUIT command and close the socket. Returns:
550 - resp: server response if successful"""
552 resp = self.shortcmd('QUIT')
553 self.file.close()
554 self.sock.close()
555 del self.file, self.sock
556 return resp
559 def _test():
560 """Minimal test function."""
561 s = NNTP('news', readermode='reader')
562 resp, count, first, last, name = s.group('comp.lang.python')
563 print resp
564 print 'Group', name, 'has', count, 'articles, range', first, 'to', last
565 resp, subs = s.xhdr('subject', first + '-' + last)
566 print resp
567 for item in subs:
568 print "%7s %s" % item
569 resp = s.quit()
570 print resp
573 # Run the test when run as a script
574 if __name__ == '__main__':
575 _test()