Results of a rewrite pass
[python/dscho.git] / Lib / nntplib.py
blob16e7550f7963be7735aef5f6f907cf13ee5ed7c0
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
35 __all__ = ["NNTP","NNTPReplyError","NNTPTemporaryError",
36 "NNTPPermanentError","NNTPProtocolError","NNTPDataError",
37 "error_reply","error_temp","error_perm","error_proto",
38 "error_data",]
40 # Exceptions raised when an error or invalid response is received
41 class NNTPError(Exception):
42 """Base class for all nntplib exceptions"""
43 def __init__(self, *args):
44 apply(Exception.__init__, (self,)+args)
45 try:
46 self.response = args[0]
47 except IndexError:
48 self.response = 'No response given'
50 class NNTPReplyError(NNTPError):
51 """Unexpected [123]xx reply"""
52 pass
54 class NNTPTemporaryError(NNTPError):
55 """4xx errors"""
56 pass
58 class NNTPPermanentError(NNTPError):
59 """5xx errors"""
60 pass
62 class NNTPProtocolError(NNTPError):
63 """Response does not begin with [1-5]"""
64 pass
66 class NNTPDataError(NNTPError):
67 """Error in response data"""
68 pass
70 # for backwards compatibility
71 error_reply = NNTPReplyError
72 error_temp = NNTPTemporaryError
73 error_perm = NNTPPermanentError
74 error_proto = NNTPProtocolError
75 error_data = NNTPDataError
79 # Standard port used by NNTP servers
80 NNTP_PORT = 119
83 # Response numbers that are followed by additional text (e.g. article)
84 LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
87 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
88 CRLF = '\r\n'
92 # The class itself
93 class NNTP:
94 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
95 readermode=None):
96 """Initialize an instance. Arguments:
97 - host: hostname to connect to
98 - port: port to connect to (default the standard NNTP port)
99 - user: username to authenticate with
100 - password: password to use with username
101 - readermode: if true, send 'mode reader' command after
102 connecting.
104 readermode is sometimes necessary if you are connecting to an
105 NNTP server on the local machine and intend to call
106 reader-specific comamnds, such as `group'. If you get
107 unexpected NNTPPermanentErrors, you might need to set
108 readermode.
110 self.host = host
111 self.port = port
112 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
113 self.sock.connect((self.host, self.port))
114 self.file = self.sock.makefile('rb')
115 self.debugging = 0
116 self.welcome = self.getresp()
118 # 'mode reader' is sometimes necessary to enable 'reader' mode.
119 # However, the order in which 'mode reader' and 'authinfo' need to
120 # arrive differs between some NNTP servers. Try to send
121 # 'mode reader', and if it fails with an authorization failed
122 # error, try again after sending authinfo.
123 readermode_afterauth = 0
124 if readermode:
125 try:
126 self.welcome = self.shortcmd('mode reader')
127 except NNTPPermanentError:
128 # error 500, probably 'not implemented'
129 pass
130 except NNTPTemporaryError, e:
131 if user and e.response[:3] == '480':
132 # Need authorization before 'mode reader'
133 readermode_afterauth = 1
134 else:
135 raise
136 # If no login/password was specified, try to get them from ~/.netrc
137 # Presume that if .netc has an entry, NNRP authentication is required.
138 try:
139 if not user:
140 import netrc
141 credentials = netrc.netrc()
142 auth = credentials.authenticators(host)
143 if auth:
144 user = auth[0]
145 password = auth[2]
146 except IOError:
147 pass
148 # Perform NNRP authentication if needed.
149 if user:
150 resp = self.shortcmd('authinfo user '+user)
151 if resp[:3] == '381':
152 if not password:
153 raise NNTPReplyError(resp)
154 else:
155 resp = self.shortcmd(
156 'authinfo pass '+password)
157 if resp[:3] != '281':
158 raise NNTPPermanentError(resp)
159 if readermode_afterauth:
160 try:
161 self.welcome = self.shortcmd('mode reader')
162 except NNTPPermanentError:
163 # error 500, probably 'not implemented'
164 pass
167 # Get the welcome message from the server
168 # (this is read and squirreled away by __init__()).
169 # If the response code is 200, posting is allowed;
170 # if it 201, posting is not allowed
172 def getwelcome(self):
173 """Get the welcome message from the server
174 (this is read and squirreled away by __init__()).
175 If the response code is 200, posting is allowed;
176 if it 201, posting is not allowed."""
178 if self.debugging: print '*welcome*', `self.welcome`
179 return self.welcome
181 def set_debuglevel(self, level):
182 """Set the debugging level. Argument 'level' means:
183 0: no debugging output (default)
184 1: print commands and responses but not body text etc.
185 2: also print raw lines read and sent before stripping CR/LF"""
187 self.debugging = level
188 debug = set_debuglevel
190 def putline(self, line):
191 """Internal: send one line to the server, appending CRLF."""
192 line = line + CRLF
193 if self.debugging > 1: print '*put*', `line`
194 self.sock.sendall(line)
196 def putcmd(self, line):
197 """Internal: send one command to the server (through putline())."""
198 if self.debugging: print '*cmd*', `line`
199 self.putline(line)
201 def getline(self):
202 """Internal: return one line from the server, stripping CRLF.
203 Raise EOFError if the connection is closed."""
204 line = self.file.readline()
205 if self.debugging > 1:
206 print '*get*', `line`
207 if not line: raise EOFError
208 if line[-2:] == CRLF: line = line[:-2]
209 elif line[-1:] in CRLF: line = line[:-1]
210 return line
212 def getresp(self):
213 """Internal: get a response from the server.
214 Raise various errors if the response indicates an error."""
215 resp = self.getline()
216 if self.debugging: print '*resp*', `resp`
217 c = resp[:1]
218 if c == '4':
219 raise NNTPTemporaryError(resp)
220 if c == '5':
221 raise NNTPPermanentError(resp)
222 if c not in '123':
223 raise NNTPProtocolError(resp)
224 return resp
226 def getlongresp(self, file=None):
227 """Internal: get a response plus following text from the server.
228 Raise various errors if the response indicates an error."""
230 openedFile = None
231 try:
232 # If a string was passed then open a file with that name
233 if isinstance(file, str):
234 openedFile = file = open(file, "w")
236 resp = self.getresp()
237 if resp[:3] not in LONGRESP:
238 raise NNTPReplyError(resp)
239 list = []
240 while 1:
241 line = self.getline()
242 if line == '.':
243 break
244 if line[:2] == '..':
245 line = line[1:]
246 if file:
247 file.write(line + "\n")
248 else:
249 list.append(line)
250 finally:
251 # If this method created the file, then it must close it
252 if openedFile:
253 openedFile.close()
255 return resp, list
257 def shortcmd(self, line):
258 """Internal: send a command and get the response."""
259 self.putcmd(line)
260 return self.getresp()
262 def longcmd(self, line, file=None):
263 """Internal: send a command and get the response plus following text."""
264 self.putcmd(line)
265 return self.getlongresp(file)
267 def newgroups(self, date, time):
268 """Process a NEWGROUPS command. Arguments:
269 - date: string 'yymmdd' indicating the date
270 - time: string 'hhmmss' indicating the time
271 Return:
272 - resp: server response if successful
273 - list: list of newsgroup names"""
275 return self.longcmd('NEWGROUPS ' + date + ' ' + time)
277 def newnews(self, group, date, time):
278 """Process a NEWNEWS command. Arguments:
279 - group: group name or '*'
280 - date: string 'yymmdd' indicating the date
281 - time: string 'hhmmss' indicating the time
282 Return:
283 - resp: server response if successful
284 - list: list of article ids"""
286 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
287 return self.longcmd(cmd)
289 def list(self):
290 """Process a LIST command. Return:
291 - resp: server response if successful
292 - list: list of (group, last, first, flag) (strings)"""
294 resp, list = self.longcmd('LIST')
295 for i in range(len(list)):
296 # Parse lines into "group last first flag"
297 list[i] = tuple(list[i].split())
298 return resp, list
300 def group(self, name):
301 """Process a GROUP command. Argument:
302 - group: the group name
303 Returns:
304 - resp: server response if successful
305 - count: number of articles (string)
306 - first: first article number (string)
307 - last: last article number (string)
308 - name: the group name"""
310 resp = self.shortcmd('GROUP ' + name)
311 if resp[:3] != '211':
312 raise NNTPReplyError(resp)
313 words = resp.split()
314 count = first = last = 0
315 n = len(words)
316 if n > 1:
317 count = words[1]
318 if n > 2:
319 first = words[2]
320 if n > 3:
321 last = words[3]
322 if n > 4:
323 name = words[4].lower()
324 return resp, count, first, last, name
326 def help(self):
327 """Process a HELP command. Returns:
328 - resp: server response if successful
329 - list: list of strings"""
331 return self.longcmd('HELP')
333 def statparse(self, resp):
334 """Internal: parse the response of a STAT, NEXT or LAST command."""
335 if resp[:2] != '22':
336 raise NNTPReplyError(resp)
337 words = resp.split()
338 nr = 0
339 id = ''
340 n = len(words)
341 if n > 1:
342 nr = words[1]
343 if n > 2:
344 id = words[2]
345 return resp, nr, id
347 def statcmd(self, line):
348 """Internal: process a STAT, NEXT or LAST command."""
349 resp = self.shortcmd(line)
350 return self.statparse(resp)
352 def stat(self, id):
353 """Process a STAT command. Argument:
354 - id: article number or message id
355 Returns:
356 - resp: server response if successful
357 - nr: the article number
358 - id: the article id"""
360 return self.statcmd('STAT ' + id)
362 def next(self):
363 """Process a NEXT command. No arguments. Return as for STAT."""
364 return self.statcmd('NEXT')
366 def last(self):
367 """Process a LAST command. No arguments. Return as for STAT."""
368 return self.statcmd('LAST')
370 def artcmd(self, line, file=None):
371 """Internal: process a HEAD, BODY or ARTICLE command."""
372 resp, list = self.longcmd(line, file)
373 resp, nr, id = self.statparse(resp)
374 return resp, nr, id, list
376 def head(self, id):
377 """Process a HEAD command. Argument:
378 - id: article number or message id
379 Returns:
380 - resp: server response if successful
381 - nr: article number
382 - id: message id
383 - list: the lines of the article's header"""
385 return self.artcmd('HEAD ' + id)
387 def body(self, id, file=None):
388 """Process a BODY command. Argument:
389 - id: article number or message id
390 - file: Filename string or file object to store the article in
391 Returns:
392 - resp: server response if successful
393 - nr: article number
394 - id: message id
395 - list: the lines of the article's body or an empty list
396 if file was used"""
398 return self.artcmd('BODY ' + id, file)
400 def article(self, id):
401 """Process an ARTICLE command. Argument:
402 - id: article number or message id
403 Returns:
404 - resp: server response if successful
405 - nr: article number
406 - id: message id
407 - list: the lines of the article"""
409 return self.artcmd('ARTICLE ' + id)
411 def slave(self):
412 """Process a SLAVE command. Returns:
413 - resp: server response if successful"""
415 return self.shortcmd('SLAVE')
417 def xhdr(self, hdr, str):
418 """Process an XHDR command (optional server extension). Arguments:
419 - hdr: the header type (e.g. 'subject')
420 - str: an article nr, a message id, or a range nr1-nr2
421 Returns:
422 - resp: server response if successful
423 - list: list of (nr, value) strings"""
425 pat = re.compile('^([0-9]+) ?(.*)\n?')
426 resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str)
427 for i in range(len(lines)):
428 line = lines[i]
429 m = pat.match(line)
430 if m:
431 lines[i] = m.group(1, 2)
432 return resp, lines
434 def xover(self,start,end):
435 """Process an XOVER command (optional server extension) Arguments:
436 - start: start of range
437 - end: end of range
438 Returns:
439 - resp: server response if successful
440 - list: list of (art-nr, subject, poster, date,
441 id, references, size, lines)"""
443 resp, lines = self.longcmd('XOVER ' + start + '-' + end)
444 xover_lines = []
445 for line in lines:
446 elem = line.split("\t")
447 try:
448 xover_lines.append((elem[0],
449 elem[1],
450 elem[2],
451 elem[3],
452 elem[4],
453 elem[5].split(),
454 elem[6],
455 elem[7]))
456 except IndexError:
457 raise NNTPDataError(line)
458 return resp,xover_lines
460 def xgtitle(self, group):
461 """Process an XGTITLE command (optional server extension) Arguments:
462 - group: group name wildcard (i.e. news.*)
463 Returns:
464 - resp: server response if successful
465 - list: list of (name,title) strings"""
467 line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
468 resp, raw_lines = self.longcmd('XGTITLE ' + group)
469 lines = []
470 for raw_line in raw_lines:
471 match = line_pat.search(raw_line.strip())
472 if match:
473 lines.append(match.group(1, 2))
474 return resp, lines
476 def xpath(self,id):
477 """Process an XPATH command (optional server extension) Arguments:
478 - id: Message id of article
479 Returns:
480 resp: server response if successful
481 path: directory path to article"""
483 resp = self.shortcmd("XPATH " + id)
484 if resp[:3] != '223':
485 raise NNTPReplyError(resp)
486 try:
487 [resp_num, path] = resp.split()
488 except ValueError:
489 raise NNTPReplyError(resp)
490 else:
491 return resp, path
493 def date (self):
494 """Process the DATE command. Arguments:
495 None
496 Returns:
497 resp: server response if successful
498 date: Date suitable for newnews/newgroups commands etc.
499 time: Time suitable for newnews/newgroups commands etc."""
501 resp = self.shortcmd("DATE")
502 if resp[:3] != '111':
503 raise NNTPReplyError(resp)
504 elem = resp.split()
505 if len(elem) != 2:
506 raise NNTPDataError(resp)
507 date = elem[1][2:8]
508 time = elem[1][-6:]
509 if len(date) != 6 or len(time) != 6:
510 raise NNTPDataError(resp)
511 return resp, date, time
514 def post(self, f):
515 """Process a POST command. Arguments:
516 - f: file containing the article
517 Returns:
518 - resp: server response if successful"""
520 resp = self.shortcmd('POST')
521 # Raises error_??? if posting is not allowed
522 if resp[0] != '3':
523 raise NNTPReplyError(resp)
524 while 1:
525 line = f.readline()
526 if not line:
527 break
528 if line[-1] == '\n':
529 line = line[:-1]
530 if line[:1] == '.':
531 line = '.' + line
532 self.putline(line)
533 self.putline('.')
534 return self.getresp()
536 def ihave(self, id, f):
537 """Process an IHAVE command. Arguments:
538 - id: message-id of the article
539 - f: file containing the article
540 Returns:
541 - resp: server response if successful
542 Note that if the server refuses the article an exception is raised."""
544 resp = self.shortcmd('IHAVE ' + id)
545 # Raises error_??? if the server already has it
546 if resp[0] != '3':
547 raise NNTPReplyError(resp)
548 while 1:
549 line = f.readline()
550 if not line:
551 break
552 if line[-1] == '\n':
553 line = line[:-1]
554 if line[:1] == '.':
555 line = '.' + line
556 self.putline(line)
557 self.putline('.')
558 return self.getresp()
560 def quit(self):
561 """Process a QUIT command and close the socket. Returns:
562 - resp: server response if successful"""
564 resp = self.shortcmd('QUIT')
565 self.file.close()
566 self.sock.close()
567 del self.file, self.sock
568 return resp
571 # Test retrieval when run as a script.
572 # Assumption: if there's a local news server, it's called 'news'.
573 # Assumption: if user queries a remote news server, it's named
574 # in the environment variable NNTPSERVER (used by slrn and kin)
575 # and we want readermode off.
576 if __name__ == '__main__':
577 import os
578 newshost = 'news' and os.environ["NNTPSERVER"]
579 if newshost.find('.') == -1:
580 mode = 'readermode'
581 else:
582 mode = None
583 s = NNTP(newshost, readermode=mode)
584 resp, count, first, last, name = s.group('comp.lang.python')
585 print resp
586 print 'Group', name, 'has', count, 'articles, range', first, 'to', last
587 resp, subs = s.xhdr('subject', first + '-' + last)
588 print resp
589 for item in subs:
590 print "%7s %s" % item
591 resp = s.quit()
592 print resp