Issue #7133: SSL objects now support the new buffer API.
[python.git] / Lib / nntplib.py
blobf519b06e55b0fbcbda7c29f5f0bfe94260139fbf
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 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, usenetrc=True):
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.create_connection((host, port))
113 self.file = self.sock.makefile('rb')
114 self.debugging = 0
115 self.welcome = self.getresp()
117 # 'mode reader' is sometimes necessary to enable 'reader' mode.
118 # However, the order in which 'mode reader' and 'authinfo' need to
119 # arrive differs between some NNTP servers. Try to send
120 # 'mode reader', and if it fails with an authorization failed
121 # error, try again after sending authinfo.
122 readermode_afterauth = 0
123 if readermode:
124 try:
125 self.welcome = self.shortcmd('mode reader')
126 except NNTPPermanentError:
127 # error 500, probably 'not implemented'
128 pass
129 except NNTPTemporaryError, e:
130 if user and e.response[:3] == '480':
131 # Need authorization before 'mode reader'
132 readermode_afterauth = 1
133 else:
134 raise
135 # If no login/password was specified, try to get them from ~/.netrc
136 # Presume that if .netc has an entry, NNRP authentication is required.
137 try:
138 if usenetrc and not user:
139 import netrc
140 credentials = netrc.netrc()
141 auth = credentials.authenticators(host)
142 if auth:
143 user = auth[0]
144 password = auth[2]
145 except IOError:
146 pass
147 # Perform NNRP authentication if needed.
148 if user:
149 resp = self.shortcmd('authinfo user '+user)
150 if resp[:3] == '381':
151 if not password:
152 raise NNTPReplyError(resp)
153 else:
154 resp = self.shortcmd(
155 'authinfo pass '+password)
156 if resp[:3] != '281':
157 raise NNTPPermanentError(resp)
158 if readermode_afterauth:
159 try:
160 self.welcome = self.shortcmd('mode reader')
161 except NNTPPermanentError:
162 # error 500, probably 'not implemented'
163 pass
166 # Get the welcome message from the server
167 # (this is read and squirreled away by __init__()).
168 # If the response code is 200, posting is allowed;
169 # if it 201, posting is not allowed
171 def getwelcome(self):
172 """Get the welcome message from the server
173 (this is read and squirreled away by __init__()).
174 If the response code is 200, posting is allowed;
175 if it 201, posting is not allowed."""
177 if self.debugging: print '*welcome*', repr(self.welcome)
178 return self.welcome
180 def set_debuglevel(self, level):
181 """Set the debugging level. Argument 'level' means:
182 0: no debugging output (default)
183 1: print commands and responses but not body text etc.
184 2: also print raw lines read and sent before stripping CR/LF"""
186 self.debugging = level
187 debug = set_debuglevel
189 def putline(self, line):
190 """Internal: send one line to the server, appending CRLF."""
191 line = line + CRLF
192 if self.debugging > 1: print '*put*', repr(line)
193 self.sock.sendall(line)
195 def putcmd(self, line):
196 """Internal: send one command to the server (through putline())."""
197 if self.debugging: print '*cmd*', repr(line)
198 self.putline(line)
200 def getline(self):
201 """Internal: return one line from the server, stripping CRLF.
202 Raise EOFError if the connection is closed."""
203 line = self.file.readline()
204 if self.debugging > 1:
205 print '*get*', repr(line)
206 if not line: raise EOFError
207 if line[-2:] == CRLF: line = line[:-2]
208 elif line[-1:] in CRLF: line = line[:-1]
209 return line
211 def getresp(self):
212 """Internal: get a response from the server.
213 Raise various errors if the response indicates an error."""
214 resp = self.getline()
215 if self.debugging: print '*resp*', repr(resp)
216 c = resp[:1]
217 if c == '4':
218 raise NNTPTemporaryError(resp)
219 if c == '5':
220 raise NNTPPermanentError(resp)
221 if c not in '123':
222 raise NNTPProtocolError(resp)
223 return resp
225 def getlongresp(self, file=None):
226 """Internal: get a response plus following text from the server.
227 Raise various errors if the response indicates an error."""
229 openedFile = None
230 try:
231 # If a string was passed then open a file with that name
232 if isinstance(file, str):
233 openedFile = file = open(file, "w")
235 resp = self.getresp()
236 if resp[:3] not in LONGRESP:
237 raise NNTPReplyError(resp)
238 list = []
239 while 1:
240 line = self.getline()
241 if line == '.':
242 break
243 if line[:2] == '..':
244 line = line[1:]
245 if file:
246 file.write(line + "\n")
247 else:
248 list.append(line)
249 finally:
250 # If this method created the file, then it must close it
251 if openedFile:
252 openedFile.close()
254 return resp, list
256 def shortcmd(self, line):
257 """Internal: send a command and get the response."""
258 self.putcmd(line)
259 return self.getresp()
261 def longcmd(self, line, file=None):
262 """Internal: send a command and get the response plus following text."""
263 self.putcmd(line)
264 return self.getlongresp(file)
266 def newgroups(self, date, time, file=None):
267 """Process a NEWGROUPS command. Arguments:
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 newsgroup names"""
274 return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
276 def newnews(self, group, date, time, file=None):
277 """Process a NEWNEWS command. Arguments:
278 - group: group name or '*'
279 - date: string 'yymmdd' indicating the date
280 - time: string 'hhmmss' indicating the time
281 Return:
282 - resp: server response if successful
283 - list: list of message ids"""
285 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
286 return self.longcmd(cmd, file)
288 def list(self, file=None):
289 """Process a LIST command. Return:
290 - resp: server response if successful
291 - list: list of (group, last, first, flag) (strings)"""
293 resp, list = self.longcmd('LIST', file)
294 for i in range(len(list)):
295 # Parse lines into "group last first flag"
296 list[i] = tuple(list[i].split())
297 return resp, list
299 def description(self, group):
301 """Get a description for a single group. If more than one
302 group matches ('group' is a pattern), return the first. If no
303 group matches, return an empty string.
305 This elides the response code from the server, since it can
306 only be '215' or '285' (for xgtitle) anyway. If the response
307 code is needed, use the 'descriptions' method.
309 NOTE: This neither checks for a wildcard in 'group' nor does
310 it check whether the group actually exists."""
312 resp, lines = self.descriptions(group)
313 if len(lines) == 0:
314 return ""
315 else:
316 return lines[0][1]
318 def descriptions(self, group_pattern):
319 """Get descriptions for a range of groups."""
320 line_pat = re.compile("^(?P<group>[^ \t]+)[ \t]+(.*)$")
321 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
322 resp, raw_lines = self.longcmd('LIST NEWSGROUPS ' + group_pattern)
323 if resp[:3] != "215":
324 # Now the deprecated XGTITLE. This either raises an error
325 # or succeeds with the same output structure as LIST
326 # NEWSGROUPS.
327 resp, raw_lines = self.longcmd('XGTITLE ' + group_pattern)
328 lines = []
329 for raw_line in raw_lines:
330 match = line_pat.search(raw_line.strip())
331 if match:
332 lines.append(match.group(1, 2))
333 return resp, lines
335 def group(self, name):
336 """Process a GROUP command. Argument:
337 - group: the group name
338 Returns:
339 - resp: server response if successful
340 - count: number of articles (string)
341 - first: first article number (string)
342 - last: last article number (string)
343 - name: the group name"""
345 resp = self.shortcmd('GROUP ' + name)
346 if resp[:3] != '211':
347 raise NNTPReplyError(resp)
348 words = resp.split()
349 count = first = last = 0
350 n = len(words)
351 if n > 1:
352 count = words[1]
353 if n > 2:
354 first = words[2]
355 if n > 3:
356 last = words[3]
357 if n > 4:
358 name = words[4].lower()
359 return resp, count, first, last, name
361 def help(self, file=None):
362 """Process a HELP command. Returns:
363 - resp: server response if successful
364 - list: list of strings"""
366 return self.longcmd('HELP',file)
368 def statparse(self, resp):
369 """Internal: parse the response of a STAT, NEXT or LAST command."""
370 if resp[:2] != '22':
371 raise NNTPReplyError(resp)
372 words = resp.split()
373 nr = 0
374 id = ''
375 n = len(words)
376 if n > 1:
377 nr = words[1]
378 if n > 2:
379 id = words[2]
380 return resp, nr, id
382 def statcmd(self, line):
383 """Internal: process a STAT, NEXT or LAST command."""
384 resp = self.shortcmd(line)
385 return self.statparse(resp)
387 def stat(self, id):
388 """Process a STAT command. Argument:
389 - id: article number or message id
390 Returns:
391 - resp: server response if successful
392 - nr: the article number
393 - id: the message id"""
395 return self.statcmd('STAT ' + id)
397 def next(self):
398 """Process a NEXT command. No arguments. Return as for STAT."""
399 return self.statcmd('NEXT')
401 def last(self):
402 """Process a LAST command. No arguments. Return as for STAT."""
403 return self.statcmd('LAST')
405 def artcmd(self, line, file=None):
406 """Internal: process a HEAD, BODY or ARTICLE command."""
407 resp, list = self.longcmd(line, file)
408 resp, nr, id = self.statparse(resp)
409 return resp, nr, id, list
411 def head(self, id):
412 """Process a HEAD command. Argument:
413 - id: article number or message id
414 Returns:
415 - resp: server response if successful
416 - nr: article number
417 - id: message id
418 - list: the lines of the article's header"""
420 return self.artcmd('HEAD ' + id)
422 def body(self, id, file=None):
423 """Process a BODY command. Argument:
424 - id: article number or message id
425 - file: Filename string or file object to store the article in
426 Returns:
427 - resp: server response if successful
428 - nr: article number
429 - id: message id
430 - list: the lines of the article's body or an empty list
431 if file was used"""
433 return self.artcmd('BODY ' + id, file)
435 def article(self, id):
436 """Process an ARTICLE command. Argument:
437 - id: article number or message id
438 Returns:
439 - resp: server response if successful
440 - nr: article number
441 - id: message id
442 - list: the lines of the article"""
444 return self.artcmd('ARTICLE ' + id)
446 def slave(self):
447 """Process a SLAVE command. Returns:
448 - resp: server response if successful"""
450 return self.shortcmd('SLAVE')
452 def xhdr(self, hdr, str, file=None):
453 """Process an XHDR command (optional server extension). Arguments:
454 - hdr: the header type (e.g. 'subject')
455 - str: an article nr, a message id, or a range nr1-nr2
456 Returns:
457 - resp: server response if successful
458 - list: list of (nr, value) strings"""
460 pat = re.compile('^([0-9]+) ?(.*)\n?')
461 resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str, file)
462 for i in range(len(lines)):
463 line = lines[i]
464 m = pat.match(line)
465 if m:
466 lines[i] = m.group(1, 2)
467 return resp, lines
469 def xover(self, start, end, file=None):
470 """Process an XOVER command (optional server extension) Arguments:
471 - start: start of range
472 - end: end of range
473 Returns:
474 - resp: server response if successful
475 - list: list of (art-nr, subject, poster, date,
476 id, references, size, lines)"""
478 resp, lines = self.longcmd('XOVER ' + start + '-' + end, file)
479 xover_lines = []
480 for line in lines:
481 elem = line.split("\t")
482 try:
483 xover_lines.append((elem[0],
484 elem[1],
485 elem[2],
486 elem[3],
487 elem[4],
488 elem[5].split(),
489 elem[6],
490 elem[7]))
491 except IndexError:
492 raise NNTPDataError(line)
493 return resp,xover_lines
495 def xgtitle(self, group, file=None):
496 """Process an XGTITLE command (optional server extension) Arguments:
497 - group: group name wildcard (i.e. news.*)
498 Returns:
499 - resp: server response if successful
500 - list: list of (name,title) strings"""
502 line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
503 resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
504 lines = []
505 for raw_line in raw_lines:
506 match = line_pat.search(raw_line.strip())
507 if match:
508 lines.append(match.group(1, 2))
509 return resp, lines
511 def xpath(self,id):
512 """Process an XPATH command (optional server extension) Arguments:
513 - id: Message id of article
514 Returns:
515 resp: server response if successful
516 path: directory path to article"""
518 resp = self.shortcmd("XPATH " + id)
519 if resp[:3] != '223':
520 raise NNTPReplyError(resp)
521 try:
522 [resp_num, path] = resp.split()
523 except ValueError:
524 raise NNTPReplyError(resp)
525 else:
526 return resp, path
528 def date (self):
529 """Process the DATE command. Arguments:
530 None
531 Returns:
532 resp: server response if successful
533 date: Date suitable for newnews/newgroups commands etc.
534 time: Time suitable for newnews/newgroups commands etc."""
536 resp = self.shortcmd("DATE")
537 if resp[:3] != '111':
538 raise NNTPReplyError(resp)
539 elem = resp.split()
540 if len(elem) != 2:
541 raise NNTPDataError(resp)
542 date = elem[1][2:8]
543 time = elem[1][-6:]
544 if len(date) != 6 or len(time) != 6:
545 raise NNTPDataError(resp)
546 return resp, date, time
549 def post(self, f):
550 """Process a POST command. Arguments:
551 - f: file containing the article
552 Returns:
553 - resp: server response if successful"""
555 resp = self.shortcmd('POST')
556 # Raises error_??? if posting is not allowed
557 if resp[0] != '3':
558 raise NNTPReplyError(resp)
559 while 1:
560 line = f.readline()
561 if not line:
562 break
563 if line[-1] == '\n':
564 line = line[:-1]
565 if line[:1] == '.':
566 line = '.' + line
567 self.putline(line)
568 self.putline('.')
569 return self.getresp()
571 def ihave(self, id, f):
572 """Process an IHAVE command. Arguments:
573 - id: message-id of the article
574 - f: file containing the article
575 Returns:
576 - resp: server response if successful
577 Note that if the server refuses the article an exception is raised."""
579 resp = self.shortcmd('IHAVE ' + id)
580 # Raises error_??? if the server already has it
581 if resp[0] != '3':
582 raise NNTPReplyError(resp)
583 while 1:
584 line = f.readline()
585 if not line:
586 break
587 if line[-1] == '\n':
588 line = line[:-1]
589 if line[:1] == '.':
590 line = '.' + line
591 self.putline(line)
592 self.putline('.')
593 return self.getresp()
595 def quit(self):
596 """Process a QUIT command and close the socket. Returns:
597 - resp: server response if successful"""
599 resp = self.shortcmd('QUIT')
600 self.file.close()
601 self.sock.close()
602 del self.file, self.sock
603 return resp
606 # Test retrieval when run as a script.
607 # Assumption: if there's a local news server, it's called 'news'.
608 # Assumption: if user queries a remote news server, it's named
609 # in the environment variable NNTPSERVER (used by slrn and kin)
610 # and we want readermode off.
611 if __name__ == '__main__':
612 import os
613 newshost = 'news' and os.environ["NNTPSERVER"]
614 if newshost.find('.') == -1:
615 mode = 'readermode'
616 else:
617 mode = None
618 s = NNTP(newshost, readermode=mode)
619 resp, count, first, last, name = s.group('comp.lang.python')
620 print resp
621 print 'Group', name, 'has', count, 'articles, range', first, 'to', last
622 resp, subs = s.xhdr('subject', first + '-' + last)
623 print resp
624 for item in subs:
625 print "%7s %s" % item
626 resp = s.quit()
627 print resp