source: python/trunk/Lib/nntplib.py@ 610

Last change on this file since 610 was 391, checked in by dmik, 11 years ago

python: Merge vendor 2.7.6 to trunk.

  • Property svn:eol-style set to native
File size: 21.0 KB
Line 
1"""An NNTP client class based on RFC 977: Network News Transfer Protocol.
2
3Example:
4
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
9Group comp.lang.python has 51 articles, range 5770 to 5821
10>>> resp, subs = s.xhdr('subject', first + '-' + last)
11>>> resp = s.quit()
12>>>
13
14Here '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
29
30
31# Imports
32import re
33import socket
34
35__all__ = ["NNTP","NNTPReplyError","NNTPTemporaryError",
36 "NNTPPermanentError","NNTPProtocolError","NNTPDataError",
37 "error_reply","error_temp","error_perm","error_proto",
38 "error_data",]
39
40# maximal line length when calling readline(). This is to prevent
41# reading arbitrary length lines. RFC 3977 limits NNTP line length to
42# 512 characters, including CRLF. We have selected 2048 just to be on
43# the safe side.
44_MAXLINE = 2048
45
46
47# Exceptions raised when an error or invalid response is received
48class NNTPError(Exception):
49 """Base class for all nntplib exceptions"""
50 def __init__(self, *args):
51 Exception.__init__(self, *args)
52 try:
53 self.response = args[0]
54 except IndexError:
55 self.response = 'No response given'
56
57class NNTPReplyError(NNTPError):
58 """Unexpected [123]xx reply"""
59 pass
60
61class NNTPTemporaryError(NNTPError):
62 """4xx errors"""
63 pass
64
65class NNTPPermanentError(NNTPError):
66 """5xx errors"""
67 pass
68
69class NNTPProtocolError(NNTPError):
70 """Response does not begin with [1-5]"""
71 pass
72
73class NNTPDataError(NNTPError):
74 """Error in response data"""
75 pass
76
77# for backwards compatibility
78error_reply = NNTPReplyError
79error_temp = NNTPTemporaryError
80error_perm = NNTPPermanentError
81error_proto = NNTPProtocolError
82error_data = NNTPDataError
83
84
85
86# Standard port used by NNTP servers
87NNTP_PORT = 119
88
89
90# Response numbers that are followed by additional text (e.g. article)
91LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
92
93
94# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
95CRLF = '\r\n'
96
97
98
99# The class itself
100class NNTP:
101 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
102 readermode=None, usenetrc=True):
103 """Initialize an instance. Arguments:
104 - host: hostname to connect to
105 - port: port to connect to (default the standard NNTP port)
106 - user: username to authenticate with
107 - password: password to use with username
108 - readermode: if true, send 'mode reader' command after
109 connecting.
110
111 readermode is sometimes necessary if you are connecting to an
112 NNTP server on the local machine and intend to call
113 reader-specific commands, such as `group'. If you get
114 unexpected NNTPPermanentErrors, you might need to set
115 readermode.
116 """
117 self.host = host
118 self.port = port
119 self.sock = socket.create_connection((host, port))
120 self.file = self.sock.makefile('rb')
121 self.debugging = 0
122 self.welcome = self.getresp()
123
124 # 'mode reader' is sometimes necessary to enable 'reader' mode.
125 # However, the order in which 'mode reader' and 'authinfo' need to
126 # arrive differs between some NNTP servers. Try to send
127 # 'mode reader', and if it fails with an authorization failed
128 # error, try again after sending authinfo.
129 readermode_afterauth = 0
130 if readermode:
131 try:
132 self.welcome = self.shortcmd('mode reader')
133 except NNTPPermanentError:
134 # error 500, probably 'not implemented'
135 pass
136 except NNTPTemporaryError, e:
137 if user and e.response[:3] == '480':
138 # Need authorization before 'mode reader'
139 readermode_afterauth = 1
140 else:
141 raise
142 # If no login/password was specified, try to get them from ~/.netrc
143 # Presume that if .netc has an entry, NNRP authentication is required.
144 try:
145 if usenetrc and not user:
146 import netrc
147 credentials = netrc.netrc()
148 auth = credentials.authenticators(host)
149 if auth:
150 user = auth[0]
151 password = auth[2]
152 except IOError:
153 pass
154 # Perform NNRP authentication if needed.
155 if user:
156 resp = self.shortcmd('authinfo user '+user)
157 if resp[:3] == '381':
158 if not password:
159 raise NNTPReplyError(resp)
160 else:
161 resp = self.shortcmd(
162 'authinfo pass '+password)
163 if resp[:3] != '281':
164 raise NNTPPermanentError(resp)
165 if readermode_afterauth:
166 try:
167 self.welcome = self.shortcmd('mode reader')
168 except NNTPPermanentError:
169 # error 500, probably 'not implemented'
170 pass
171
172
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
177
178 def getwelcome(self):
179 """Get the welcome message from the server
180 (this is read and squirreled away by __init__()).
181 If the response code is 200, posting is allowed;
182 if it 201, posting is not allowed."""
183
184 if self.debugging: print '*welcome*', repr(self.welcome)
185 return self.welcome
186
187 def set_debuglevel(self, level):
188 """Set the debugging level. Argument 'level' means:
189 0: no debugging output (default)
190 1: print commands and responses but not body text etc.
191 2: also print raw lines read and sent before stripping CR/LF"""
192
193 self.debugging = level
194 debug = set_debuglevel
195
196 def putline(self, line):
197 """Internal: send one line to the server, appending CRLF."""
198 line = line + CRLF
199 if self.debugging > 1: print '*put*', repr(line)
200 self.sock.sendall(line)
201
202 def putcmd(self, line):
203 """Internal: send one command to the server (through putline())."""
204 if self.debugging: print '*cmd*', repr(line)
205 self.putline(line)
206
207 def getline(self):
208 """Internal: return one line from the server, stripping CRLF.
209 Raise EOFError if the connection is closed."""
210 line = self.file.readline(_MAXLINE + 1)
211 if len(line) > _MAXLINE:
212 raise NNTPDataError('line too long')
213 if self.debugging > 1:
214 print '*get*', repr(line)
215 if not line: raise EOFError
216 if line[-2:] == CRLF: line = line[:-2]
217 elif line[-1:] in CRLF: line = line[:-1]
218 return line
219
220 def getresp(self):
221 """Internal: get a response from the server.
222 Raise various errors if the response indicates an error."""
223 resp = self.getline()
224 if self.debugging: print '*resp*', repr(resp)
225 c = resp[:1]
226 if c == '4':
227 raise NNTPTemporaryError(resp)
228 if c == '5':
229 raise NNTPPermanentError(resp)
230 if c not in '123':
231 raise NNTPProtocolError(resp)
232 return resp
233
234 def getlongresp(self, file=None):
235 """Internal: get a response plus following text from the server.
236 Raise various errors if the response indicates an error."""
237
238 openedFile = None
239 try:
240 # If a string was passed then open a file with that name
241 if isinstance(file, str):
242 openedFile = file = open(file, "w")
243
244 resp = self.getresp()
245 if resp[:3] not in LONGRESP:
246 raise NNTPReplyError(resp)
247 list = []
248 while 1:
249 line = self.getline()
250 if line == '.':
251 break
252 if line[:2] == '..':
253 line = line[1:]
254 if file:
255 file.write(line + "\n")
256 else:
257 list.append(line)
258 finally:
259 # If this method created the file, then it must close it
260 if openedFile:
261 openedFile.close()
262
263 return resp, list
264
265 def shortcmd(self, line):
266 """Internal: send a command and get the response."""
267 self.putcmd(line)
268 return self.getresp()
269
270 def longcmd(self, line, file=None):
271 """Internal: send a command and get the response plus following text."""
272 self.putcmd(line)
273 return self.getlongresp(file)
274
275 def newgroups(self, date, time, file=None):
276 """Process a NEWGROUPS command. Arguments:
277 - date: string 'yymmdd' indicating the date
278 - time: string 'hhmmss' indicating the time
279 Return:
280 - resp: server response if successful
281 - list: list of newsgroup names"""
282
283 return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
284
285 def newnews(self, group, date, time, file=None):
286 """Process a NEWNEWS command. Arguments:
287 - group: group name or '*'
288 - date: string 'yymmdd' indicating the date
289 - time: string 'hhmmss' indicating the time
290 Return:
291 - resp: server response if successful
292 - list: list of message ids"""
293
294 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
295 return self.longcmd(cmd, file)
296
297 def list(self, file=None):
298 """Process a LIST command. Return:
299 - resp: server response if successful
300 - list: list of (group, last, first, flag) (strings)"""
301
302 resp, list = self.longcmd('LIST', file)
303 for i in range(len(list)):
304 # Parse lines into "group last first flag"
305 list[i] = tuple(list[i].split())
306 return resp, list
307
308 def description(self, group):
309
310 """Get a description for a single group. If more than one
311 group matches ('group' is a pattern), return the first. If no
312 group matches, return an empty string.
313
314 This elides the response code from the server, since it can
315 only be '215' or '285' (for xgtitle) anyway. If the response
316 code is needed, use the 'descriptions' method.
317
318 NOTE: This neither checks for a wildcard in 'group' nor does
319 it check whether the group actually exists."""
320
321 resp, lines = self.descriptions(group)
322 if len(lines) == 0:
323 return ""
324 else:
325 return lines[0][1]
326
327 def descriptions(self, group_pattern):
328 """Get descriptions for a range of groups."""
329 line_pat = re.compile("^(?P<group>[^ \t]+)[ \t]+(.*)$")
330 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
331 resp, raw_lines = self.longcmd('LIST NEWSGROUPS ' + group_pattern)
332 if resp[:3] != "215":
333 # Now the deprecated XGTITLE. This either raises an error
334 # or succeeds with the same output structure as LIST
335 # NEWSGROUPS.
336 resp, raw_lines = self.longcmd('XGTITLE ' + group_pattern)
337 lines = []
338 for raw_line in raw_lines:
339 match = line_pat.search(raw_line.strip())
340 if match:
341 lines.append(match.group(1, 2))
342 return resp, lines
343
344 def group(self, name):
345 """Process a GROUP command. Argument:
346 - group: the group name
347 Returns:
348 - resp: server response if successful
349 - count: number of articles (string)
350 - first: first article number (string)
351 - last: last article number (string)
352 - name: the group name"""
353
354 resp = self.shortcmd('GROUP ' + name)
355 if resp[:3] != '211':
356 raise NNTPReplyError(resp)
357 words = resp.split()
358 count = first = last = 0
359 n = len(words)
360 if n > 1:
361 count = words[1]
362 if n > 2:
363 first = words[2]
364 if n > 3:
365 last = words[3]
366 if n > 4:
367 name = words[4].lower()
368 return resp, count, first, last, name
369
370 def help(self, file=None):
371 """Process a HELP command. Returns:
372 - resp: server response if successful
373 - list: list of strings"""
374
375 return self.longcmd('HELP',file)
376
377 def statparse(self, resp):
378 """Internal: parse the response of a STAT, NEXT or LAST command."""
379 if resp[:2] != '22':
380 raise NNTPReplyError(resp)
381 words = resp.split()
382 nr = 0
383 id = ''
384 n = len(words)
385 if n > 1:
386 nr = words[1]
387 if n > 2:
388 id = words[2]
389 return resp, nr, id
390
391 def statcmd(self, line):
392 """Internal: process a STAT, NEXT or LAST command."""
393 resp = self.shortcmd(line)
394 return self.statparse(resp)
395
396 def stat(self, id):
397 """Process a STAT command. Argument:
398 - id: article number or message id
399 Returns:
400 - resp: server response if successful
401 - nr: the article number
402 - id: the message id"""
403
404 return self.statcmd('STAT ' + id)
405
406 def next(self):
407 """Process a NEXT command. No arguments. Return as for STAT."""
408 return self.statcmd('NEXT')
409
410 def last(self):
411 """Process a LAST command. No arguments. Return as for STAT."""
412 return self.statcmd('LAST')
413
414 def artcmd(self, line, file=None):
415 """Internal: process a HEAD, BODY or ARTICLE command."""
416 resp, list = self.longcmd(line, file)
417 resp, nr, id = self.statparse(resp)
418 return resp, nr, id, list
419
420 def head(self, id):
421 """Process a HEAD command. Argument:
422 - id: article number or message id
423 Returns:
424 - resp: server response if successful
425 - nr: article number
426 - id: message id
427 - list: the lines of the article's header"""
428
429 return self.artcmd('HEAD ' + id)
430
431 def body(self, id, file=None):
432 """Process a BODY command. Argument:
433 - id: article number or message id
434 - file: Filename string or file object to store the article in
435 Returns:
436 - resp: server response if successful
437 - nr: article number
438 - id: message id
439 - list: the lines of the article's body or an empty list
440 if file was used"""
441
442 return self.artcmd('BODY ' + id, file)
443
444 def article(self, id):
445 """Process an ARTICLE command. Argument:
446 - id: article number or message id
447 Returns:
448 - resp: server response if successful
449 - nr: article number
450 - id: message id
451 - list: the lines of the article"""
452
453 return self.artcmd('ARTICLE ' + id)
454
455 def slave(self):
456 """Process a SLAVE command. Returns:
457 - resp: server response if successful"""
458
459 return self.shortcmd('SLAVE')
460
461 def xhdr(self, hdr, str, file=None):
462 """Process an XHDR command (optional server extension). Arguments:
463 - hdr: the header type (e.g. 'subject')
464 - str: an article nr, a message id, or a range nr1-nr2
465 Returns:
466 - resp: server response if successful
467 - list: list of (nr, value) strings"""
468
469 pat = re.compile('^([0-9]+) ?(.*)\n?')
470 resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str, file)
471 for i in range(len(lines)):
472 line = lines[i]
473 m = pat.match(line)
474 if m:
475 lines[i] = m.group(1, 2)
476 return resp, lines
477
478 def xover(self, start, end, file=None):
479 """Process an XOVER command (optional server extension) Arguments:
480 - start: start of range
481 - end: end of range
482 Returns:
483 - resp: server response if successful
484 - list: list of (art-nr, subject, poster, date,
485 id, references, size, lines)"""
486
487 resp, lines = self.longcmd('XOVER ' + start + '-' + end, file)
488 xover_lines = []
489 for line in lines:
490 elem = line.split("\t")
491 try:
492 xover_lines.append((elem[0],
493 elem[1],
494 elem[2],
495 elem[3],
496 elem[4],
497 elem[5].split(),
498 elem[6],
499 elem[7]))
500 except IndexError:
501 raise NNTPDataError(line)
502 return resp,xover_lines
503
504 def xgtitle(self, group, file=None):
505 """Process an XGTITLE command (optional server extension) Arguments:
506 - group: group name wildcard (i.e. news.*)
507 Returns:
508 - resp: server response if successful
509 - list: list of (name,title) strings"""
510
511 line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
512 resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
513 lines = []
514 for raw_line in raw_lines:
515 match = line_pat.search(raw_line.strip())
516 if match:
517 lines.append(match.group(1, 2))
518 return resp, lines
519
520 def xpath(self,id):
521 """Process an XPATH command (optional server extension) Arguments:
522 - id: Message id of article
523 Returns:
524 resp: server response if successful
525 path: directory path to article"""
526
527 resp = self.shortcmd("XPATH " + id)
528 if resp[:3] != '223':
529 raise NNTPReplyError(resp)
530 try:
531 [resp_num, path] = resp.split()
532 except ValueError:
533 raise NNTPReplyError(resp)
534 else:
535 return resp, path
536
537 def date (self):
538 """Process the DATE command. Arguments:
539 None
540 Returns:
541 resp: server response if successful
542 date: Date suitable for newnews/newgroups commands etc.
543 time: Time suitable for newnews/newgroups commands etc."""
544
545 resp = self.shortcmd("DATE")
546 if resp[:3] != '111':
547 raise NNTPReplyError(resp)
548 elem = resp.split()
549 if len(elem) != 2:
550 raise NNTPDataError(resp)
551 date = elem[1][2:8]
552 time = elem[1][-6:]
553 if len(date) != 6 or len(time) != 6:
554 raise NNTPDataError(resp)
555 return resp, date, time
556
557
558 def post(self, f):
559 """Process a POST command. Arguments:
560 - f: file containing the article
561 Returns:
562 - resp: server response if successful"""
563
564 resp = self.shortcmd('POST')
565 # Raises error_??? if posting is not allowed
566 if resp[0] != '3':
567 raise NNTPReplyError(resp)
568 while 1:
569 line = f.readline()
570 if not line:
571 break
572 if line[-1] == '\n':
573 line = line[:-1]
574 if line[:1] == '.':
575 line = '.' + line
576 self.putline(line)
577 self.putline('.')
578 return self.getresp()
579
580 def ihave(self, id, f):
581 """Process an IHAVE command. Arguments:
582 - id: message-id of the article
583 - f: file containing the article
584 Returns:
585 - resp: server response if successful
586 Note that if the server refuses the article an exception is raised."""
587
588 resp = self.shortcmd('IHAVE ' + id)
589 # Raises error_??? if the server already has it
590 if resp[0] != '3':
591 raise NNTPReplyError(resp)
592 while 1:
593 line = f.readline()
594 if not line:
595 break
596 if line[-1] == '\n':
597 line = line[:-1]
598 if line[:1] == '.':
599 line = '.' + line
600 self.putline(line)
601 self.putline('.')
602 return self.getresp()
603
604 def quit(self):
605 """Process a QUIT command and close the socket. Returns:
606 - resp: server response if successful"""
607
608 resp = self.shortcmd('QUIT')
609 self.file.close()
610 self.sock.close()
611 del self.file, self.sock
612 return resp
613
614
615# Test retrieval when run as a script.
616# Assumption: if there's a local news server, it's called 'news'.
617# Assumption: if user queries a remote news server, it's named
618# in the environment variable NNTPSERVER (used by slrn and kin)
619# and we want readermode off.
620if __name__ == '__main__':
621 import os
622 newshost = 'news' and os.environ["NNTPSERVER"]
623 if newshost.find('.') == -1:
624 mode = 'readermode'
625 else:
626 mode = None
627 s = NNTP(newshost, readermode=mode)
628 resp, count, first, last, name = s.group('comp.lang.python')
629 print resp
630 print 'Group', name, 'has', count, 'articles, range', first, 'to', last
631 resp, subs = s.xhdr('subject', first + '-' + last)
632 print resp
633 for item in subs:
634 print "%7s %s" % item
635 resp = s.quit()
636 print resp
Note: See TracBrowser for help on using the repository browser.