- Got rid of newmodule.c
[python/dscho.git] / Lib / nntplib.py
blobf09c69a42192bb8d314fb4c6c33e7f94690e560e
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 user:
137 resp = self.shortcmd('authinfo user '+user)
138 if resp[:3] == '381':
139 if not password:
140 raise NNTPReplyError(resp)
141 else:
142 resp = self.shortcmd(
143 'authinfo pass '+password)
144 if resp[:3] != '281':
145 raise NNTPPermanentError(resp)
146 if readermode_afterauth:
147 try:
148 self.welcome = self.shortcmd('mode reader')
149 except NNTPPermanentError:
150 # error 500, probably 'not implemented'
151 pass
154 # Get the welcome message from the server
155 # (this is read and squirreled away by __init__()).
156 # If the response code is 200, posting is allowed;
157 # if it 201, posting is not allowed
159 def getwelcome(self):
160 """Get the welcome message from the server
161 (this is read and squirreled away by __init__()).
162 If the response code is 200, posting is allowed;
163 if it 201, posting is not allowed."""
165 if self.debugging: print '*welcome*', `self.welcome`
166 return self.welcome
168 def set_debuglevel(self, level):
169 """Set the debugging level. Argument 'level' means:
170 0: no debugging output (default)
171 1: print commands and responses but not body text etc.
172 2: also print raw lines read and sent before stripping CR/LF"""
174 self.debugging = level
175 debug = set_debuglevel
177 def putline(self, line):
178 """Internal: send one line to the server, appending CRLF."""
179 line = line + CRLF
180 if self.debugging > 1: print '*put*', `line`
181 self.sock.sendall(line)
183 def putcmd(self, line):
184 """Internal: send one command to the server (through putline())."""
185 if self.debugging: print '*cmd*', `line`
186 self.putline(line)
188 def getline(self):
189 """Internal: return one line from the server, stripping CRLF.
190 Raise EOFError if the connection is closed."""
191 line = self.file.readline()
192 if self.debugging > 1:
193 print '*get*', `line`
194 if not line: raise EOFError
195 if line[-2:] == CRLF: line = line[:-2]
196 elif line[-1:] in CRLF: line = line[:-1]
197 return line
199 def getresp(self):
200 """Internal: get a response from the server.
201 Raise various errors if the response indicates an error."""
202 resp = self.getline()
203 if self.debugging: print '*resp*', `resp`
204 c = resp[:1]
205 if c == '4':
206 raise NNTPTemporaryError(resp)
207 if c == '5':
208 raise NNTPPermanentError(resp)
209 if c not in '123':
210 raise NNTPProtocolError(resp)
211 return resp
213 def getlongresp(self, file=None):
214 """Internal: get a response plus following text from the server.
215 Raise various errors if the response indicates an error."""
217 openedFile = None
218 try:
219 # If a string was passed then open a file with that name
220 if isinstance(file, str):
221 openedFile = file = open(file, "w")
223 resp = self.getresp()
224 if resp[:3] not in LONGRESP:
225 raise NNTPReplyError(resp)
226 list = []
227 while 1:
228 line = self.getline()
229 if line == '.':
230 break
231 if line[:2] == '..':
232 line = line[1:]
233 if file:
234 file.write(line + "\n")
235 else:
236 list.append(line)
237 finally:
238 # If this method created the file, then it must close it
239 if openedFile:
240 openedFile.close()
242 return resp, list
244 def shortcmd(self, line):
245 """Internal: send a command and get the response."""
246 self.putcmd(line)
247 return self.getresp()
249 def longcmd(self, line, file=None):
250 """Internal: send a command and get the response plus following text."""
251 self.putcmd(line)
252 return self.getlongresp(file)
254 def newgroups(self, date, time):
255 """Process a NEWGROUPS command. Arguments:
256 - date: string 'yymmdd' indicating the date
257 - time: string 'hhmmss' indicating the time
258 Return:
259 - resp: server response if successful
260 - list: list of newsgroup names"""
262 return self.longcmd('NEWGROUPS ' + date + ' ' + time)
264 def newnews(self, group, date, time):
265 """Process a NEWNEWS command. Arguments:
266 - group: group name or '*'
267 - date: string 'yymmdd' indicating the date
268 - time: string 'hhmmss' indicating the time
269 Return:
270 - resp: server response if successful
271 - list: list of article ids"""
273 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
274 return self.longcmd(cmd)
276 def list(self):
277 """Process a LIST command. Return:
278 - resp: server response if successful
279 - list: list of (group, last, first, flag) (strings)"""
281 resp, list = self.longcmd('LIST')
282 for i in range(len(list)):
283 # Parse lines into "group last first flag"
284 list[i] = tuple(list[i].split())
285 return resp, list
287 def group(self, name):
288 """Process a GROUP command. Argument:
289 - group: the group name
290 Returns:
291 - resp: server response if successful
292 - count: number of articles (string)
293 - first: first article number (string)
294 - last: last article number (string)
295 - name: the group name"""
297 resp = self.shortcmd('GROUP ' + name)
298 if resp[:3] != '211':
299 raise NNTPReplyError(resp)
300 words = resp.split()
301 count = first = last = 0
302 n = len(words)
303 if n > 1:
304 count = words[1]
305 if n > 2:
306 first = words[2]
307 if n > 3:
308 last = words[3]
309 if n > 4:
310 name = words[4].lower()
311 return resp, count, first, last, name
313 def help(self):
314 """Process a HELP command. Returns:
315 - resp: server response if successful
316 - list: list of strings"""
318 return self.longcmd('HELP')
320 def statparse(self, resp):
321 """Internal: parse the response of a STAT, NEXT or LAST command."""
322 if resp[:2] != '22':
323 raise NNTPReplyError(resp)
324 words = resp.split()
325 nr = 0
326 id = ''
327 n = len(words)
328 if n > 1:
329 nr = words[1]
330 if n > 2:
331 id = words[2]
332 return resp, nr, id
334 def statcmd(self, line):
335 """Internal: process a STAT, NEXT or LAST command."""
336 resp = self.shortcmd(line)
337 return self.statparse(resp)
339 def stat(self, id):
340 """Process a STAT command. Argument:
341 - id: article number or message id
342 Returns:
343 - resp: server response if successful
344 - nr: the article number
345 - id: the article id"""
347 return self.statcmd('STAT ' + id)
349 def next(self):
350 """Process a NEXT command. No arguments. Return as for STAT."""
351 return self.statcmd('NEXT')
353 def last(self):
354 """Process a LAST command. No arguments. Return as for STAT."""
355 return self.statcmd('LAST')
357 def artcmd(self, line, file=None):
358 """Internal: process a HEAD, BODY or ARTICLE command."""
359 resp, list = self.longcmd(line, file)
360 resp, nr, id = self.statparse(resp)
361 return resp, nr, id, list
363 def head(self, id):
364 """Process a HEAD command. Argument:
365 - id: article number or message id
366 Returns:
367 - resp: server response if successful
368 - nr: article number
369 - id: message id
370 - list: the lines of the article's header"""
372 return self.artcmd('HEAD ' + id)
374 def body(self, id, file=None):
375 """Process a BODY command. Argument:
376 - id: article number or message id
377 - file: Filename string or file object to store the article in
378 Returns:
379 - resp: server response if successful
380 - nr: article number
381 - id: message id
382 - list: the lines of the article's body or an empty list
383 if file was used"""
385 return self.artcmd('BODY ' + id, file)
387 def article(self, id):
388 """Process an ARTICLE command. Argument:
389 - id: article number or message id
390 Returns:
391 - resp: server response if successful
392 - nr: article number
393 - id: message id
394 - list: the lines of the article"""
396 return self.artcmd('ARTICLE ' + id)
398 def slave(self):
399 """Process a SLAVE command. Returns:
400 - resp: server response if successful"""
402 return self.shortcmd('SLAVE')
404 def xhdr(self, hdr, str):
405 """Process an XHDR command (optional server extension). Arguments:
406 - hdr: the header type (e.g. 'subject')
407 - str: an article nr, a message id, or a range nr1-nr2
408 Returns:
409 - resp: server response if successful
410 - list: list of (nr, value) strings"""
412 pat = re.compile('^([0-9]+) ?(.*)\n?')
413 resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str)
414 for i in range(len(lines)):
415 line = lines[i]
416 m = pat.match(line)
417 if m:
418 lines[i] = m.group(1, 2)
419 return resp, lines
421 def xover(self,start,end):
422 """Process an XOVER command (optional server extension) Arguments:
423 - start: start of range
424 - end: end of range
425 Returns:
426 - resp: server response if successful
427 - list: list of (art-nr, subject, poster, date,
428 id, references, size, lines)"""
430 resp, lines = self.longcmd('XOVER ' + start + '-' + end)
431 xover_lines = []
432 for line in lines:
433 elem = line.split("\t")
434 try:
435 xover_lines.append((elem[0],
436 elem[1],
437 elem[2],
438 elem[3],
439 elem[4],
440 elem[5].split(),
441 elem[6],
442 elem[7]))
443 except IndexError:
444 raise NNTPDataError(line)
445 return resp,xover_lines
447 def xgtitle(self, group):
448 """Process an XGTITLE command (optional server extension) Arguments:
449 - group: group name wildcard (i.e. news.*)
450 Returns:
451 - resp: server response if successful
452 - list: list of (name,title) strings"""
454 line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
455 resp, raw_lines = self.longcmd('XGTITLE ' + group)
456 lines = []
457 for raw_line in raw_lines:
458 match = line_pat.search(raw_line.strip())
459 if match:
460 lines.append(match.group(1, 2))
461 return resp, lines
463 def xpath(self,id):
464 """Process an XPATH command (optional server extension) Arguments:
465 - id: Message id of article
466 Returns:
467 resp: server response if successful
468 path: directory path to article"""
470 resp = self.shortcmd("XPATH " + id)
471 if resp[:3] != '223':
472 raise NNTPReplyError(resp)
473 try:
474 [resp_num, path] = resp.split()
475 except ValueError:
476 raise NNTPReplyError(resp)
477 else:
478 return resp, path
480 def date (self):
481 """Process the DATE command. Arguments:
482 None
483 Returns:
484 resp: server response if successful
485 date: Date suitable for newnews/newgroups commands etc.
486 time: Time suitable for newnews/newgroups commands etc."""
488 resp = self.shortcmd("DATE")
489 if resp[:3] != '111':
490 raise NNTPReplyError(resp)
491 elem = resp.split()
492 if len(elem) != 2:
493 raise NNTPDataError(resp)
494 date = elem[1][2:8]
495 time = elem[1][-6:]
496 if len(date) != 6 or len(time) != 6:
497 raise NNTPDataError(resp)
498 return resp, date, time
501 def post(self, f):
502 """Process a POST command. Arguments:
503 - f: file containing the article
504 Returns:
505 - resp: server response if successful"""
507 resp = self.shortcmd('POST')
508 # Raises error_??? if posting is not allowed
509 if resp[0] != '3':
510 raise NNTPReplyError(resp)
511 while 1:
512 line = f.readline()
513 if not line:
514 break
515 if line[-1] == '\n':
516 line = line[:-1]
517 if line[:1] == '.':
518 line = '.' + line
519 self.putline(line)
520 self.putline('.')
521 return self.getresp()
523 def ihave(self, id, f):
524 """Process an IHAVE command. Arguments:
525 - id: message-id of the article
526 - f: file containing the article
527 Returns:
528 - resp: server response if successful
529 Note that if the server refuses the article an exception is raised."""
531 resp = self.shortcmd('IHAVE ' + id)
532 # Raises error_??? if the server already has it
533 if resp[0] != '3':
534 raise NNTPReplyError(resp)
535 while 1:
536 line = f.readline()
537 if not line:
538 break
539 if line[-1] == '\n':
540 line = line[:-1]
541 if line[:1] == '.':
542 line = '.' + line
543 self.putline(line)
544 self.putline('.')
545 return self.getresp()
547 def quit(self):
548 """Process a QUIT command and close the socket. Returns:
549 - resp: server response if successful"""
551 resp = self.shortcmd('QUIT')
552 self.file.close()
553 self.sock.close()
554 del self.file, self.sock
555 return resp
558 def _test():
559 """Minimal test function."""
560 s = NNTP('news', readermode='reader')
561 resp, count, first, last, name = s.group('comp.lang.python')
562 print resp
563 print 'Group', name, 'has', count, 'articles, range', first, 'to', last
564 resp, subs = s.xhdr('subject', first + '-' + last)
565 print resp
566 for item in subs:
567 print "%7s %s" % item
568 resp = s.quit()
569 print resp
572 # Run the test when run as a script
573 if __name__ == '__main__':
574 _test()