blob: f519b06e55b0fbcbda7c29f5f0bfe94260139fbf [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
Antoine Pitroueed30d82009-05-14 21:22:08 +0000112 self.sock = socket.create_connection((host, port))
Tim Peters2344fae2001-01-15 00:50:52 +0000113 self.file = self.sock.makefile('rb')
114 self.debugging = 0
115 self.welcome = self.getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000116
Thomas Wouters47adcba2001-01-16 06:35:14 +0000117 # 'mode reader' is sometimes necessary to enable 'reader' mode.
Tim Petersdfb673b2001-01-16 07:12:46 +0000118 # However, the order in which 'mode reader' and 'authinfo' need to
Thomas Wouters47adcba2001-01-16 06:35:14 +0000119 # 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
Tim Peters2344fae2001-01-15 00:50:52 +0000123 if readermode:
124 try:
125 self.welcome = self.shortcmd('mode reader')
126 except NNTPPermanentError:
127 # error 500, probably 'not implemented'
128 pass
Thomas Wouters47adcba2001-01-16 06:35:14 +0000129 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
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000135 # If no login/password was specified, try to get them from ~/.netrc
136 # Presume that if .netc has an entry, NNRP authentication is required.
Eric S. Raymond782d9402002-11-17 17:53:12 +0000137 try:
Martin v. Löwis9513e342004-08-03 14:36:32 +0000138 if usenetrc and not user:
Eric S. Raymond782d9402002-11-17 17:53:12 +0000139 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
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000147 # Perform NNRP authentication if needed.
Tim Peters2344fae2001-01-15 00:50:52 +0000148 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)
Thomas Wouters47adcba2001-01-16 06:35:14 +0000158 if readermode_afterauth:
159 try:
160 self.welcome = self.shortcmd('mode reader')
161 except NNTPPermanentError:
162 # error 500, probably 'not implemented'
163 pass
Tim Petersdfb673b2001-01-16 07:12:46 +0000164
Barry Warsaw9dd78722000-02-10 20:25:53 +0000165
Tim Peters2344fae2001-01-15 00:50:52 +0000166 # 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
Guido van Rossumc629d341992-11-05 10:43:02 +0000170
Tim Peters2344fae2001-01-15 00:50:52 +0000171 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."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000176
Walter Dörwald70a6b492004-02-12 17:35:32 +0000177 if self.debugging: print '*welcome*', repr(self.welcome)
Tim Peters2344fae2001-01-15 00:50:52 +0000178 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000179
Tim Peters2344fae2001-01-15 00:50:52 +0000180 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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000185
Tim Peters2344fae2001-01-15 00:50:52 +0000186 self.debugging = level
187 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000188
Tim Peters2344fae2001-01-15 00:50:52 +0000189 def putline(self, line):
190 """Internal: send one line to the server, appending CRLF."""
191 line = line + CRLF
Walter Dörwald70a6b492004-02-12 17:35:32 +0000192 if self.debugging > 1: print '*put*', repr(line)
Martin v. Löwise12454f2002-02-16 23:06:19 +0000193 self.sock.sendall(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000194
Tim Peters2344fae2001-01-15 00:50:52 +0000195 def putcmd(self, line):
196 """Internal: send one command to the server (through putline())."""
Walter Dörwald70a6b492004-02-12 17:35:32 +0000197 if self.debugging: print '*cmd*', repr(line)
Tim Peters2344fae2001-01-15 00:50:52 +0000198 self.putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000199
Tim Peters2344fae2001-01-15 00:50:52 +0000200 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:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000205 print '*get*', repr(line)
Tim Peters2344fae2001-01-15 00:50:52 +0000206 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
Guido van Rossumc629d341992-11-05 10:43:02 +0000210
Tim Peters2344fae2001-01-15 00:50:52 +0000211 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()
Walter Dörwald70a6b492004-02-12 17:35:32 +0000215 if self.debugging: print '*resp*', repr(resp)
Tim Peters2344fae2001-01-15 00:50:52 +0000216 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
Guido van Rossumc629d341992-11-05 10:43:02 +0000224
Fredrik Lundha5e61652001-10-18 20:58:25 +0000225 def getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000226 """Internal: get a response plus following text from the server.
227 Raise various errors if the response indicates an error."""
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000228
229 openedFile = None
230 try:
231 # If a string was passed then open a file with that name
Walter Dörwald65230a22002-06-03 15:58:32 +0000232 if isinstance(file, str):
Fredrik Lundha5e61652001-10-18 20:58:25 +0000233 openedFile = file = open(file, "w")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000234
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:]
Fredrik Lundha5e61652001-10-18 20:58:25 +0000245 if file:
246 file.write(line + "\n")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000247 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()
253
Tim Peters2344fae2001-01-15 00:50:52 +0000254 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000255
Tim Peters2344fae2001-01-15 00:50:52 +0000256 def shortcmd(self, line):
257 """Internal: send a command and get the response."""
258 self.putcmd(line)
259 return self.getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000260
Fredrik Lundha5e61652001-10-18 20:58:25 +0000261 def longcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000262 """Internal: send a command and get the response plus following text."""
263 self.putcmd(line)
Fredrik Lundha5e61652001-10-18 20:58:25 +0000264 return self.getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000265
Guido van Rossuma2685402003-04-19 18:04:57 +0000266 def newgroups(self, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000267 """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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000273
Guido van Rossuma2685402003-04-19 18:04:57 +0000274 return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000275
Guido van Rossuma2685402003-04-19 18:04:57 +0000276 def newnews(self, group, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000277 """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
Georg Brandl5dbda752005-07-17 20:27:41 +0000283 - list: list of message ids"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000284
Tim Peters2344fae2001-01-15 00:50:52 +0000285 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
Guido van Rossuma2685402003-04-19 18:04:57 +0000286 return self.longcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000287
Guido van Rossuma2685402003-04-19 18:04:57 +0000288 def list(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000289 """Process a LIST command. Return:
290 - resp: server response if successful
291 - list: list of (group, last, first, flag) (strings)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000292
Guido van Rossuma2685402003-04-19 18:04:57 +0000293 resp, list = self.longcmd('LIST', file)
Tim Peters2344fae2001-01-15 00:50:52 +0000294 for i in range(len(list)):
295 # Parse lines into "group last first flag"
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000296 list[i] = tuple(list[i].split())
Tim Peters2344fae2001-01-15 00:50:52 +0000297 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000298
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000299 def description(self, group):
300
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.
304
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.
308
309 NOTE: This neither checks for a wildcard in 'group' nor does
310 it check whether the group actually exists."""
311
312 resp, lines = self.descriptions(group)
313 if len(lines) == 0:
314 return ""
315 else:
316 return lines[0][1]
317
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
334
Tim Peters2344fae2001-01-15 00:50:52 +0000335 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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000344
Tim Peters2344fae2001-01-15 00:50:52 +0000345 resp = self.shortcmd('GROUP ' + name)
346 if resp[:3] != '211':
347 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000348 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000349 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:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000358 name = words[4].lower()
Tim Peters2344fae2001-01-15 00:50:52 +0000359 return resp, count, first, last, name
Guido van Rossumc629d341992-11-05 10:43:02 +0000360
Guido van Rossuma2685402003-04-19 18:04:57 +0000361 def help(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000362 """Process a HELP command. Returns:
363 - resp: server response if successful
364 - list: list of strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000365
Guido van Rossuma2685402003-04-19 18:04:57 +0000366 return self.longcmd('HELP',file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000367
Tim Peters2344fae2001-01-15 00:50:52 +0000368 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)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000372 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000373 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
Guido van Rossumc629d341992-11-05 10:43:02 +0000381
Tim Peters2344fae2001-01-15 00:50:52 +0000382 def statcmd(self, line):
383 """Internal: process a STAT, NEXT or LAST command."""
384 resp = self.shortcmd(line)
385 return self.statparse(resp)
Guido van Rossumc629d341992-11-05 10:43:02 +0000386
Tim Peters2344fae2001-01-15 00:50:52 +0000387 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
Georg Brandl5dbda752005-07-17 20:27:41 +0000393 - id: the message id"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000394
Tim Peters2344fae2001-01-15 00:50:52 +0000395 return self.statcmd('STAT ' + id)
Guido van Rossumc629d341992-11-05 10:43:02 +0000396
Tim Peters2344fae2001-01-15 00:50:52 +0000397 def next(self):
398 """Process a NEXT command. No arguments. Return as for STAT."""
399 return self.statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000400
Tim Peters2344fae2001-01-15 00:50:52 +0000401 def last(self):
402 """Process a LAST command. No arguments. Return as for STAT."""
403 return self.statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000404
Fredrik Lundha5e61652001-10-18 20:58:25 +0000405 def artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000406 """Internal: process a HEAD, BODY or ARTICLE command."""
Fredrik Lundha5e61652001-10-18 20:58:25 +0000407 resp, list = self.longcmd(line, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000408 resp, nr, id = self.statparse(resp)
409 return resp, nr, id, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000410
Tim Peters2344fae2001-01-15 00:50:52 +0000411 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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000419
Tim Peters2344fae2001-01-15 00:50:52 +0000420 return self.artcmd('HEAD ' + id)
Guido van Rossumc629d341992-11-05 10:43:02 +0000421
Fredrik Lundha5e61652001-10-18 20:58:25 +0000422 def body(self, id, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000423 """Process a BODY command. Argument:
424 - id: article number or message id
Fredrik Lundha5e61652001-10-18 20:58:25 +0000425 - file: Filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000426 Returns:
427 - resp: server response if successful
428 - nr: article number
429 - id: message id
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000430 - list: the lines of the article's body or an empty list
Fredrik Lundha5e61652001-10-18 20:58:25 +0000431 if file was used"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000432
Fredrik Lundha5e61652001-10-18 20:58:25 +0000433 return self.artcmd('BODY ' + id, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000434
Tim Peters2344fae2001-01-15 00:50:52 +0000435 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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000443
Tim Peters2344fae2001-01-15 00:50:52 +0000444 return self.artcmd('ARTICLE ' + id)
Guido van Rossumc629d341992-11-05 10:43:02 +0000445
Tim Peters2344fae2001-01-15 00:50:52 +0000446 def slave(self):
447 """Process a SLAVE command. Returns:
448 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000449
Tim Peters2344fae2001-01-15 00:50:52 +0000450 return self.shortcmd('SLAVE')
Guido van Rossumc629d341992-11-05 10:43:02 +0000451
Guido van Rossuma2685402003-04-19 18:04:57 +0000452 def xhdr(self, hdr, str, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000453 """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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000459
Tim Peters2344fae2001-01-15 00:50:52 +0000460 pat = re.compile('^([0-9]+) ?(.*)\n?')
Guido van Rossuma2685402003-04-19 18:04:57 +0000461 resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000462 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
Guido van Rossumc629d341992-11-05 10:43:02 +0000468
Guido van Rossuma2685402003-04-19 18:04:57 +0000469 def xover(self, start, end, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000470 """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)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000477
Guido van Rossuma2685402003-04-19 18:04:57 +0000478 resp, lines = self.longcmd('XOVER ' + start + '-' + end, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000479 xover_lines = []
480 for line in lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000481 elem = line.split("\t")
Tim Peters2344fae2001-01-15 00:50:52 +0000482 try:
483 xover_lines.append((elem[0],
484 elem[1],
485 elem[2],
486 elem[3],
487 elem[4],
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000488 elem[5].split(),
Tim Peters2344fae2001-01-15 00:50:52 +0000489 elem[6],
490 elem[7]))
491 except IndexError:
492 raise NNTPDataError(line)
493 return resp,xover_lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000494
Guido van Rossuma2685402003-04-19 18:04:57 +0000495 def xgtitle(self, group, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000496 """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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000501
Tim Peters2344fae2001-01-15 00:50:52 +0000502 line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
Guido van Rossuma2685402003-04-19 18:04:57 +0000503 resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000504 lines = []
505 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000506 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000507 if match:
508 lines.append(match.group(1, 2))
509 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000510
Tim Peters2344fae2001-01-15 00:50:52 +0000511 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"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000517
Tim Peters2344fae2001-01-15 00:50:52 +0000518 resp = self.shortcmd("XPATH " + id)
519 if resp[:3] != '223':
520 raise NNTPReplyError(resp)
521 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000522 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000523 except ValueError:
524 raise NNTPReplyError(resp)
525 else:
526 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000527
Tim Peters2344fae2001-01-15 00:50:52 +0000528 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."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000535
Tim Peters2344fae2001-01-15 00:50:52 +0000536 resp = self.shortcmd("DATE")
537 if resp[:3] != '111':
538 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000539 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000540 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
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000547
548
Tim Peters2344fae2001-01-15 00:50:52 +0000549 def post(self, f):
550 """Process a POST command. Arguments:
551 - f: file containing the article
552 Returns:
553 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000554
Tim Peters2344fae2001-01-15 00:50:52 +0000555 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()
Guido van Rossumc629d341992-11-05 10:43:02 +0000570
Tim Peters2344fae2001-01-15 00:50:52 +0000571 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."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000578
Tim Peters2344fae2001-01-15 00:50:52 +0000579 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()
Guido van Rossumc629d341992-11-05 10:43:02 +0000594
Tim Peters2344fae2001-01-15 00:50:52 +0000595 def quit(self):
596 """Process a QUIT command and close the socket. Returns:
597 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000598
Tim Peters2344fae2001-01-15 00:50:52 +0000599 resp = self.shortcmd('QUIT')
600 self.file.close()
601 self.sock.close()
602 del self.file, self.sock
603 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000604
605
Neal Norwitzef679562002-11-14 02:19:44 +0000606# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000607# 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.
611if __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)
Tim Peters2344fae2001-01-15 00:50:52 +0000619 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