blob: cc51d1da8225045823b17ca276ba990eecd4c957 [file] [log] [blame]
Guido van Rossum54f22ed2000-02-04 15:10:34 +00001"""An NNTP client class based on RFC 977: Network News Transfer Protocol.
Guido van Rossumc629d341992-11-05 10:43:02 +00002
Guido van Rossum54f22ed2000-02-04 15:10:34 +00003Example:
Guido van Rossumc629d341992-11-05 10:43:02 +00004
Guido van Rossum54f22ed2000-02-04 15:10:34 +00005>>> 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>>>
Guido van Rossumc629d341992-11-05 10:43:02 +000013
Guido van Rossum54f22ed2000-02-04 15:10:34 +000014Here '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
Guido van Rossum8421c4e1995-09-22 00:52:38 +000029
Guido van Rossumc629d341992-11-05 10:43:02 +000030
31# Imports
Guido van Rossum9694fca1997-10-22 21:00:49 +000032import re
Guido van Rossumc629d341992-11-05 10:43:02 +000033import socket
Guido van Rossumc629d341992-11-05 10:43:02 +000034
Skip Montanaro269b83b2001-02-06 01:07:02 +000035__all__ = ["NNTP","NNTPReplyError","NNTPTemporaryError",
36 "NNTPPermanentError","NNTPProtocolError","NNTPDataError",
37 "error_reply","error_temp","error_perm","error_proto",
38 "error_data",]
Tim Peters2344fae2001-01-15 00:50:52 +000039
Barry Warsaw9dd78722000-02-10 20:25:53 +000040# Exceptions raised when an error or invalid response is received
41class NNTPError(Exception):
Tim Peters2344fae2001-01-15 00:50:52 +000042 """Base class for all nntplib exceptions"""
43 def __init__(self, *args):
Guido van Rossum68468eb2003-02-27 20:14:51 +000044 Exception.__init__(self, *args)
Tim Peters2344fae2001-01-15 00:50:52 +000045 try:
46 self.response = args[0]
47 except IndexError:
48 self.response = 'No response given'
Barry Warsaw9dd78722000-02-10 20:25:53 +000049
50class NNTPReplyError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000051 """Unexpected [123]xx reply"""
52 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000053
54class NNTPTemporaryError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000055 """4xx errors"""
56 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000057
58class NNTPPermanentError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000059 """5xx errors"""
60 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000061
62class NNTPProtocolError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000063 """Response does not begin with [1-5]"""
64 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000065
66class NNTPDataError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000067 """Error in response data"""
68 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000069
70# for backwards compatibility
71error_reply = NNTPReplyError
72error_temp = NNTPTemporaryError
73error_perm = NNTPPermanentError
74error_proto = NNTPProtocolError
75error_data = NNTPDataError
Guido van Rossumc629d341992-11-05 10:43:02 +000076
77
Tim Peters2344fae2001-01-15 00:50:52 +000078
Guido van Rossumc629d341992-11-05 10:43:02 +000079# Standard port used by NNTP servers
80NNTP_PORT = 119
81
82
83# Response numbers that are followed by additional text (e.g. article)
Guido van Rossum8421c4e1995-09-22 00:52:38 +000084LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
Guido van Rossumc629d341992-11-05 10:43:02 +000085
86
87# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
88CRLF = '\r\n'
89
90
Tim Peters2344fae2001-01-15 00:50:52 +000091
Guido van Rossumc629d341992-11-05 10:43:02 +000092# The class itself
Guido van Rossumc629d341992-11-05 10:43:02 +000093class NNTP:
Tim Peters2344fae2001-01-15 00:50:52 +000094 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
Martin v. Löwis9513e342004-08-03 14:36:32 +000095 readermode=None, usenetrc=True):
Tim Peters2344fae2001-01-15 00:50:52 +000096 """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.
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000103
Tim Peters2344fae2001-01-15 00:50:52 +0000104 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()
Tim Petersdfb673b2001-01-16 07:12:46 +0000117
Thomas Wouters47adcba2001-01-16 06:35:14 +0000118 # 'mode reader' is sometimes necessary to enable 'reader' mode.
Tim Petersdfb673b2001-01-16 07:12:46 +0000119 # However, the order in which 'mode reader' and 'authinfo' need to
Thomas Wouters47adcba2001-01-16 06:35:14 +0000120 # 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
Tim Peters2344fae2001-01-15 00:50:52 +0000124 if readermode:
125 try:
126 self.welcome = self.shortcmd('mode reader')
127 except NNTPPermanentError:
128 # error 500, probably 'not implemented'
129 pass
Thomas Wouters47adcba2001-01-16 06:35:14 +0000130 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
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000136 # If no login/password was specified, try to get them from ~/.netrc
137 # Presume that if .netc has an entry, NNRP authentication is required.
Eric S. Raymond782d9402002-11-17 17:53:12 +0000138 try:
Martin v. Löwis9513e342004-08-03 14:36:32 +0000139 if usenetrc and not user:
Eric S. Raymond782d9402002-11-17 17:53:12 +0000140 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
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000148 # Perform NNRP authentication if needed.
Tim Peters2344fae2001-01-15 00:50:52 +0000149 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)
Thomas Wouters47adcba2001-01-16 06:35:14 +0000159 if readermode_afterauth:
160 try:
161 self.welcome = self.shortcmd('mode reader')
162 except NNTPPermanentError:
163 # error 500, probably 'not implemented'
164 pass
Tim Petersdfb673b2001-01-16 07:12:46 +0000165
Barry Warsaw9dd78722000-02-10 20:25:53 +0000166
Tim Peters2344fae2001-01-15 00:50:52 +0000167 # 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
Guido van Rossumc629d341992-11-05 10:43:02 +0000171
Tim Peters2344fae2001-01-15 00:50:52 +0000172 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."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000177
Walter Dörwald70a6b492004-02-12 17:35:32 +0000178 if self.debugging: print '*welcome*', repr(self.welcome)
Tim Peters2344fae2001-01-15 00:50:52 +0000179 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000180
Tim Peters2344fae2001-01-15 00:50:52 +0000181 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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000186
Tim Peters2344fae2001-01-15 00:50:52 +0000187 self.debugging = level
188 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000189
Tim Peters2344fae2001-01-15 00:50:52 +0000190 def putline(self, line):
191 """Internal: send one line to the server, appending CRLF."""
192 line = line + CRLF
Walter Dörwald70a6b492004-02-12 17:35:32 +0000193 if self.debugging > 1: print '*put*', repr(line)
Martin v. Löwise12454f2002-02-16 23:06:19 +0000194 self.sock.sendall(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000195
Tim Peters2344fae2001-01-15 00:50:52 +0000196 def putcmd(self, line):
197 """Internal: send one command to the server (through putline())."""
Walter Dörwald70a6b492004-02-12 17:35:32 +0000198 if self.debugging: print '*cmd*', repr(line)
Tim Peters2344fae2001-01-15 00:50:52 +0000199 self.putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000200
Tim Peters2344fae2001-01-15 00:50:52 +0000201 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:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000206 print '*get*', repr(line)
Tim Peters2344fae2001-01-15 00:50:52 +0000207 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
Guido van Rossumc629d341992-11-05 10:43:02 +0000211
Tim Peters2344fae2001-01-15 00:50:52 +0000212 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()
Walter Dörwald70a6b492004-02-12 17:35:32 +0000216 if self.debugging: print '*resp*', repr(resp)
Tim Peters2344fae2001-01-15 00:50:52 +0000217 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
Guido van Rossumc629d341992-11-05 10:43:02 +0000225
Fredrik Lundha5e61652001-10-18 20:58:25 +0000226 def getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000227 """Internal: get a response plus following text from the server.
228 Raise various errors if the response indicates an error."""
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000229
230 openedFile = None
231 try:
232 # If a string was passed then open a file with that name
Walter Dörwald65230a22002-06-03 15:58:32 +0000233 if isinstance(file, str):
Fredrik Lundha5e61652001-10-18 20:58:25 +0000234 openedFile = file = open(file, "w")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000235
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:]
Fredrik Lundha5e61652001-10-18 20:58:25 +0000246 if file:
247 file.write(line + "\n")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000248 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
Tim Peters2344fae2001-01-15 00:50:52 +0000255 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000256
Tim Peters2344fae2001-01-15 00:50:52 +0000257 def shortcmd(self, line):
258 """Internal: send a command and get the response."""
259 self.putcmd(line)
260 return self.getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000261
Fredrik Lundha5e61652001-10-18 20:58:25 +0000262 def longcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000263 """Internal: send a command and get the response plus following text."""
264 self.putcmd(line)
Fredrik Lundha5e61652001-10-18 20:58:25 +0000265 return self.getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000266
Guido van Rossuma2685402003-04-19 18:04:57 +0000267 def newgroups(self, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000268 """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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000274
Guido van Rossuma2685402003-04-19 18:04:57 +0000275 return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000276
Guido van Rossuma2685402003-04-19 18:04:57 +0000277 def newnews(self, group, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000278 """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
Georg Brandl5dbda752005-07-17 20:27:41 +0000284 - list: list of message ids"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000285
Tim Peters2344fae2001-01-15 00:50:52 +0000286 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
Guido van Rossuma2685402003-04-19 18:04:57 +0000287 return self.longcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000288
Guido van Rossuma2685402003-04-19 18:04:57 +0000289 def list(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000290 """Process a LIST command. Return:
291 - resp: server response if successful
292 - list: list of (group, last, first, flag) (strings)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000293
Guido van Rossuma2685402003-04-19 18:04:57 +0000294 resp, list = self.longcmd('LIST', file)
Tim Peters2344fae2001-01-15 00:50:52 +0000295 for i in range(len(list)):
296 # Parse lines into "group last first flag"
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000297 list[i] = tuple(list[i].split())
Tim Peters2344fae2001-01-15 00:50:52 +0000298 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000299
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000300 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
Tim Peters2344fae2001-01-15 00:50:52 +0000336 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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000345
Tim Peters2344fae2001-01-15 00:50:52 +0000346 resp = self.shortcmd('GROUP ' + name)
347 if resp[:3] != '211':
348 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000349 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000350 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:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000359 name = words[4].lower()
Tim Peters2344fae2001-01-15 00:50:52 +0000360 return resp, count, first, last, name
Guido van Rossumc629d341992-11-05 10:43:02 +0000361
Guido van Rossuma2685402003-04-19 18:04:57 +0000362 def help(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000363 """Process a HELP command. Returns:
364 - resp: server response if successful
365 - list: list of strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000366
Guido van Rossuma2685402003-04-19 18:04:57 +0000367 return self.longcmd('HELP',file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000368
Tim Peters2344fae2001-01-15 00:50:52 +0000369 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)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000373 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000374 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
Guido van Rossumc629d341992-11-05 10:43:02 +0000382
Tim Peters2344fae2001-01-15 00:50:52 +0000383 def statcmd(self, line):
384 """Internal: process a STAT, NEXT or LAST command."""
385 resp = self.shortcmd(line)
386 return self.statparse(resp)
Guido van Rossumc629d341992-11-05 10:43:02 +0000387
Tim Peters2344fae2001-01-15 00:50:52 +0000388 def stat(self, id):
389 """Process a STAT command. Argument:
390 - id: article number or message id
391 Returns:
392 - resp: server response if successful
393 - nr: the article number
Georg Brandl5dbda752005-07-17 20:27:41 +0000394 - id: the message id"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000395
Tim Peters2344fae2001-01-15 00:50:52 +0000396 return self.statcmd('STAT ' + id)
Guido van Rossumc629d341992-11-05 10:43:02 +0000397
Tim Peters2344fae2001-01-15 00:50:52 +0000398 def next(self):
399 """Process a NEXT command. No arguments. Return as for STAT."""
400 return self.statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000401
Tim Peters2344fae2001-01-15 00:50:52 +0000402 def last(self):
403 """Process a LAST command. No arguments. Return as for STAT."""
404 return self.statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000405
Fredrik Lundha5e61652001-10-18 20:58:25 +0000406 def artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000407 """Internal: process a HEAD, BODY or ARTICLE command."""
Fredrik Lundha5e61652001-10-18 20:58:25 +0000408 resp, list = self.longcmd(line, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000409 resp, nr, id = self.statparse(resp)
410 return resp, nr, id, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000411
Tim Peters2344fae2001-01-15 00:50:52 +0000412 def head(self, id):
413 """Process a HEAD command. Argument:
414 - id: article number or message id
415 Returns:
416 - resp: server response if successful
417 - nr: article number
418 - id: message id
419 - list: the lines of the article's header"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000420
Tim Peters2344fae2001-01-15 00:50:52 +0000421 return self.artcmd('HEAD ' + id)
Guido van Rossumc629d341992-11-05 10:43:02 +0000422
Fredrik Lundha5e61652001-10-18 20:58:25 +0000423 def body(self, id, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000424 """Process a BODY command. Argument:
425 - id: article number or message id
Fredrik Lundha5e61652001-10-18 20:58:25 +0000426 - file: Filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000427 Returns:
428 - resp: server response if successful
429 - nr: article number
430 - id: message id
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000431 - list: the lines of the article's body or an empty list
Fredrik Lundha5e61652001-10-18 20:58:25 +0000432 if file was used"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000433
Fredrik Lundha5e61652001-10-18 20:58:25 +0000434 return self.artcmd('BODY ' + id, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000435
Tim Peters2344fae2001-01-15 00:50:52 +0000436 def article(self, id):
437 """Process an ARTICLE command. Argument:
438 - id: article number or message id
439 Returns:
440 - resp: server response if successful
441 - nr: article number
442 - id: message id
443 - list: the lines of the article"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000444
Tim Peters2344fae2001-01-15 00:50:52 +0000445 return self.artcmd('ARTICLE ' + id)
Guido van Rossumc629d341992-11-05 10:43:02 +0000446
Tim Peters2344fae2001-01-15 00:50:52 +0000447 def slave(self):
448 """Process a SLAVE command. Returns:
449 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000450
Tim Peters2344fae2001-01-15 00:50:52 +0000451 return self.shortcmd('SLAVE')
Guido van Rossumc629d341992-11-05 10:43:02 +0000452
Guido van Rossuma2685402003-04-19 18:04:57 +0000453 def xhdr(self, hdr, str, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000454 """Process an XHDR command (optional server extension). Arguments:
455 - hdr: the header type (e.g. 'subject')
456 - str: an article nr, a message id, or a range nr1-nr2
457 Returns:
458 - resp: server response if successful
459 - list: list of (nr, value) strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000460
Tim Peters2344fae2001-01-15 00:50:52 +0000461 pat = re.compile('^([0-9]+) ?(.*)\n?')
Guido van Rossuma2685402003-04-19 18:04:57 +0000462 resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000463 for i in range(len(lines)):
464 line = lines[i]
465 m = pat.match(line)
466 if m:
467 lines[i] = m.group(1, 2)
468 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000469
Guido van Rossuma2685402003-04-19 18:04:57 +0000470 def xover(self, start, end, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000471 """Process an XOVER command (optional server extension) Arguments:
472 - start: start of range
473 - end: end of range
474 Returns:
475 - resp: server response if successful
476 - list: list of (art-nr, subject, poster, date,
477 id, references, size, lines)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000478
Guido van Rossuma2685402003-04-19 18:04:57 +0000479 resp, lines = self.longcmd('XOVER ' + start + '-' + end, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000480 xover_lines = []
481 for line in lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000482 elem = line.split("\t")
Tim Peters2344fae2001-01-15 00:50:52 +0000483 try:
484 xover_lines.append((elem[0],
485 elem[1],
486 elem[2],
487 elem[3],
488 elem[4],
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000489 elem[5].split(),
Tim Peters2344fae2001-01-15 00:50:52 +0000490 elem[6],
491 elem[7]))
492 except IndexError:
493 raise NNTPDataError(line)
494 return resp,xover_lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000495
Guido van Rossuma2685402003-04-19 18:04:57 +0000496 def xgtitle(self, group, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000497 """Process an XGTITLE command (optional server extension) Arguments:
498 - group: group name wildcard (i.e. news.*)
499 Returns:
500 - resp: server response if successful
501 - list: list of (name,title) strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000502
Tim Peters2344fae2001-01-15 00:50:52 +0000503 line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
Guido van Rossuma2685402003-04-19 18:04:57 +0000504 resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000505 lines = []
506 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000507 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000508 if match:
509 lines.append(match.group(1, 2))
510 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000511
Tim Peters2344fae2001-01-15 00:50:52 +0000512 def xpath(self,id):
513 """Process an XPATH command (optional server extension) Arguments:
514 - id: Message id of article
515 Returns:
516 resp: server response if successful
517 path: directory path to article"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000518
Tim Peters2344fae2001-01-15 00:50:52 +0000519 resp = self.shortcmd("XPATH " + id)
520 if resp[:3] != '223':
521 raise NNTPReplyError(resp)
522 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000523 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000524 except ValueError:
525 raise NNTPReplyError(resp)
526 else:
527 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000528
Tim Peters2344fae2001-01-15 00:50:52 +0000529 def date (self):
530 """Process the DATE command. Arguments:
531 None
532 Returns:
533 resp: server response if successful
534 date: Date suitable for newnews/newgroups commands etc.
535 time: Time suitable for newnews/newgroups commands etc."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000536
Tim Peters2344fae2001-01-15 00:50:52 +0000537 resp = self.shortcmd("DATE")
538 if resp[:3] != '111':
539 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000540 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000541 if len(elem) != 2:
542 raise NNTPDataError(resp)
543 date = elem[1][2:8]
544 time = elem[1][-6:]
545 if len(date) != 6 or len(time) != 6:
546 raise NNTPDataError(resp)
547 return resp, date, time
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000548
549
Tim Peters2344fae2001-01-15 00:50:52 +0000550 def post(self, f):
551 """Process a POST command. Arguments:
552 - f: file containing the article
553 Returns:
554 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000555
Tim Peters2344fae2001-01-15 00:50:52 +0000556 resp = self.shortcmd('POST')
557 # Raises error_??? if posting is not allowed
558 if resp[0] != '3':
559 raise NNTPReplyError(resp)
560 while 1:
561 line = f.readline()
562 if not line:
563 break
564 if line[-1] == '\n':
565 line = line[:-1]
566 if line[:1] == '.':
567 line = '.' + line
568 self.putline(line)
569 self.putline('.')
570 return self.getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000571
Tim Peters2344fae2001-01-15 00:50:52 +0000572 def ihave(self, id, f):
573 """Process an IHAVE command. Arguments:
574 - id: message-id of the article
575 - f: file containing the article
576 Returns:
577 - resp: server response if successful
578 Note that if the server refuses the article an exception is raised."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000579
Tim Peters2344fae2001-01-15 00:50:52 +0000580 resp = self.shortcmd('IHAVE ' + id)
581 # Raises error_??? if the server already has it
582 if resp[0] != '3':
583 raise NNTPReplyError(resp)
584 while 1:
585 line = f.readline()
586 if not line:
587 break
588 if line[-1] == '\n':
589 line = line[:-1]
590 if line[:1] == '.':
591 line = '.' + line
592 self.putline(line)
593 self.putline('.')
594 return self.getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000595
Tim Peters2344fae2001-01-15 00:50:52 +0000596 def quit(self):
597 """Process a QUIT command and close the socket. Returns:
598 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000599
Tim Peters2344fae2001-01-15 00:50:52 +0000600 resp = self.shortcmd('QUIT')
601 self.file.close()
602 self.sock.close()
603 del self.file, self.sock
604 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000605
606
Neal Norwitzef679562002-11-14 02:19:44 +0000607# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000608# Assumption: if there's a local news server, it's called 'news'.
609# Assumption: if user queries a remote news server, it's named
610# in the environment variable NNTPSERVER (used by slrn and kin)
611# and we want readermode off.
612if __name__ == '__main__':
613 import os
614 newshost = 'news' and os.environ["NNTPSERVER"]
615 if newshost.find('.') == -1:
616 mode = 'readermode'
617 else:
618 mode = None
619 s = NNTP(newshost, readermode=mode)
Tim Peters2344fae2001-01-15 00:50:52 +0000620 resp, count, first, last, name = s.group('comp.lang.python')
621 print resp
622 print 'Group', name, 'has', count, 'articles, range', first, 'to', last
623 resp, subs = s.xhdr('subject', first + '-' + last)
624 print resp
625 for item in subs:
626 print "%7s %s" % item
627 resp = s.quit()
628 print resp