source: trunk/essentials/dev-lang/python/Lib/nntplib.py@ 3364

Last change on this file since 3364 was 3225, checked in by bird, 19 years ago

Python 2.5

File size: 20.7 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# Exceptions raised when an error or invalid response is received
41class 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'
49
50class NNTPReplyError(NNTPError):
51 """Unexpected [123]xx reply"""
52 pass
53
54class NNTPTemporaryError(NNTPError):
55 """4xx errors"""
56 pass
57
58class NNTPPermanentError(NNTPError):
59 """5xx errors"""
60 pass
61
62class NNTPProtocolError(NNTPError):
63 """Response does not begin with [1-5]"""
64 pass
65
66class NNTPDataError(NNTPError):
67 """Error in response data"""
68 pass
69
70# for backwards compatibility
71error_reply = NNTPReplyError
72error_temp = NNTPTemporaryError
73error_perm = NNTPPermanentError
74error_proto = NNTPProtocolError
75error_data = NNTPDataError
76
77
78
79# Standard port used by NNTP servers
80NNTP_PORT = 119
81
82
83# Response numbers that are followed by additional text (e.g. article)
84LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
85
86
87# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
88CRLF = '\r\n'
89
90
91
92# The class itself
93class 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.
103
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.
109 """
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()
117
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 usenetrc and 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
165
166
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
171
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."""
177
178 if self.debugging: print '*welcome*', repr(self.welcome)
179 return self.welcome
180
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"""
186
187 self.debugging = level
188 debug = set_debuglevel
189
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*', repr(line)
194 self.sock.sendall(line)
195
196 def putcmd(self, line):
197 """Internal: send one command to the server (through putline())."""
198 if self.debugging: print '*cmd*', repr(line)
199 self.putline(line)
200
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*', repr(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
211
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*', repr(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
225
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."""
229
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")
235
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()
254
255 return resp, list
256
257 def shortcmd(self, line):
258 """Internal: send a command and get the response."""
259 self.putcmd(line)
260 return self.getresp()
261
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)
266
267 def newgroups(self, date, time, file=None):
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"""
274
275 return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
276
277 def newnews(self, group, date, time, file=None):
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 message ids"""
285
286 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
287 return self.longcmd(cmd, file)
288
289 def list(self, file=None):
290 """Process a LIST command. Return:
291 - resp: server response if successful
292 - list: list of (group, last, first, flag) (strings)"""
293
294 resp, list = self.longcmd('LIST', file)
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
299
300 def description(self, group):
301
302 """Get a description for a single group. If more than one
303 group matches ('group' is a pattern), return the first. If no
304 group matches, return an empty string.
305
306 This elides the response code from the server, since it can
307 only be '215' or '285' (for xgtitle) anyway. If the response
308 code is needed, use the 'descriptions' method.
309
310 NOTE: This neither checks for a wildcard in 'group' nor does
311 it check whether the group actually exists."""
312
313 resp, lines = self.descriptions(group)
314 if len(lines) == 0:
315 return ""
316 else:
317 return lines[0][1]
318
319 def descriptions(self, group_pattern):
320 """Get descriptions for a range of groups."""
321 line_pat = re.compile("^(?P<group>[^ \t]+)[ \t]+(.*)$")
322 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
323 resp, raw_lines = self.longcmd('LIST NEWSGROUPS ' + group_pattern)
324 if resp[:3] != "215":
325 # Now the deprecated XGTITLE. This either raises an error
326 # or succeeds with the same output structure as LIST
327 # NEWSGROUPS.
328 resp, raw_lines = self.longcmd('XGTITLE ' + group_pattern)
329 lines = []
330 for raw_line in raw_lines:
331 match = line_pat.search(raw_line.strip())
332 if match:
333 lines.append(match.group(1, 2))
334 return resp, lines
335
336 def group(self, name):
337 """Process a GROUP command. Argument:
338 - group: the group name
339 Returns:
340 - resp: server response if successful
341 - count: number of articles (string)
342 - first: first article number (string)
343 - last: last article number (string)
344 - name: the group name"""
345
346 resp = self.shortcmd('GROUP ' + name)
347 if resp[:3] != '211':
348 raise NNTPReplyError(resp)
349 words = resp.split()
350 count = first = last = 0
351 n = len(words)
352 if n > 1:
353 count = words[1]
354 if n > 2:
355 first = words[2]
356 if n > 3:
357 last = words[3]
358 if n > 4:
359 name = words[4].lower()
360 return resp, count, first, last, name
361
362 def help(self, file=None):
363 """Process a HELP command. Returns:
364 - resp: server response if successful
365 - list: list of strings"""
366
367 return self.longcmd('HELP',file)
368
369 def statparse(self, resp):
370 """Internal: parse the response of a STAT, NEXT or LAST command."""
371 if resp[:2] != '22':
372 raise NNTPReplyError(resp)
373 words = resp.split()
374 nr = 0
375 id = ''
376 n = len(words)
377 if n > 1:
378 nr = words[1]
379 if n > 2:
380 id = words[2]
381 return resp, nr, id
382
383 def statcmd(self, line):
384 """Internal: process a STAT, NEXT or LAST command."""