blob: 6ac2fcb9949b7eb22f679a813365e7fa6d9749d9 [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')
Guido van Rossum7131f842007-02-09 20:13:25 +00008>>> print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Guido van Rossum54f22ed2000-02-04 15:10:34 +00009Group comp.lang.python has 51 articles, range 5770 to 5821
Christian Heimes933238a2008-11-05 19:44:21 +000010>>> resp, subs = s.xhdr('subject', '{0}-{1}'.format(first, last))
Guido van Rossum54f22ed2000-02-04 15:10:34 +000011>>> 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:
Christian Heimes933238a2008-11-05 19:44:21 +000018>>> f = open(filename, 'rb') # file containing article, including header
Guido van Rossum54f22ed2000-02-04 15:10:34 +000019>>> 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)
Christian Heimes933238a2008-11-05 19:44:21 +000084LONGRESP = [b'100', b'215', b'220', b'221', b'222', b'224', b'230', b'231', b'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)
Christian Heimes933238a2008-11-05 19:44:21 +000088CRLF = b'\r\n'
Guido van Rossumc629d341992-11-05 10:43:02 +000089
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 Pitrou2ca24f02009-05-14 21:30:46 +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
Guido van Rossumb940e112007-01-10 16:19:56 +0000129 except NNTPTemporaryError as e:
Christian Heimes933238a2008-11-05 19:44:21 +0000130 if user and e.response.startswith(b'480'):
Thomas Wouters47adcba2001-01-16 06:35:14 +0000131 # 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)
Christian Heimes933238a2008-11-05 19:44:21 +0000150 if resp.startswith(b'381'):
Tim Peters2344fae2001-01-15 00:50:52 +0000151 if not password:
152 raise NNTPReplyError(resp)
153 else:
154 resp = self.shortcmd(
155 'authinfo pass '+password)
Christian Heimes933238a2008-11-05 19:44:21 +0000156 if not resp.startswith(b'281'):
Tim Peters2344fae2001-01-15 00:50:52 +0000157 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
Guido van Rossumbe19ed72007-02-09 05:37:30 +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
Guido van Rossumbe19ed72007-02-09 05:37:30 +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())."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000197 if self.debugging: print('*cmd*', repr(line))
Christian Heimes933238a2008-11-05 19:44:21 +0000198 line = bytes(line, "ASCII")
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:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000206 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000207 if not line: raise EOFError
Christian Heimes933238a2008-11-05 19:44:21 +0000208 if line[-2:] == CRLF:
209 line = line[:-2]
210 elif line[-1:] in CRLF:
211 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000212 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000213
Tim Peters2344fae2001-01-15 00:50:52 +0000214 def getresp(self):
215 """Internal: get a response from the server.
216 Raise various errors if the response indicates an error."""
217 resp = self.getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000218 if self.debugging: print('*resp*', repr(resp))
Tim Peters2344fae2001-01-15 00:50:52 +0000219 c = resp[:1]
Christian Heimes933238a2008-11-05 19:44:21 +0000220 if c == b'4':
Tim Peters2344fae2001-01-15 00:50:52 +0000221 raise NNTPTemporaryError(resp)
Christian Heimes933238a2008-11-05 19:44:21 +0000222 if c == b'5':
Tim Peters2344fae2001-01-15 00:50:52 +0000223 raise NNTPPermanentError(resp)
Christian Heimes933238a2008-11-05 19:44:21 +0000224 if c not in b'123':
Tim Peters2344fae2001-01-15 00:50:52 +0000225 raise NNTPProtocolError(resp)
226 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000227
Fredrik Lundha5e61652001-10-18 20:58:25 +0000228 def getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000229 """Internal: get a response plus following text from the server.
230 Raise various errors if the response indicates an error."""
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000231
232 openedFile = None
233 try:
234 # If a string was passed then open a file with that name
Walter Dörwald65230a22002-06-03 15:58:32 +0000235 if isinstance(file, str):
Fredrik Lundha5e61652001-10-18 20:58:25 +0000236 openedFile = file = open(file, "w")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000237
238 resp = self.getresp()
239 if resp[:3] not in LONGRESP:
240 raise NNTPReplyError(resp)
241 list = []
242 while 1:
243 line = self.getline()
Christian Heimes933238a2008-11-05 19:44:21 +0000244 if line == b'.':
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000245 break
Christian Heimes933238a2008-11-05 19:44:21 +0000246 if line.startswith(b'..'):
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000247 line = line[1:]
Fredrik Lundha5e61652001-10-18 20:58:25 +0000248 if file:
Christian Heimes933238a2008-11-05 19:44:21 +0000249 file.write(line + b'\n')
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000250 else:
251 list.append(line)
252 finally:
253 # If this method created the file, then it must close it
254 if openedFile:
255 openedFile.close()
256
Tim Peters2344fae2001-01-15 00:50:52 +0000257 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000258
Tim Peters2344fae2001-01-15 00:50:52 +0000259 def shortcmd(self, line):
260 """Internal: send a command and get the response."""
261 self.putcmd(line)
262 return self.getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000263
Fredrik Lundha5e61652001-10-18 20:58:25 +0000264 def longcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000265 """Internal: send a command and get the response plus following text."""
266 self.putcmd(line)
Fredrik Lundha5e61652001-10-18 20:58:25 +0000267 return self.getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000268
Guido van Rossuma2685402003-04-19 18:04:57 +0000269 def newgroups(self, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000270 """Process a NEWGROUPS command. Arguments:
271 - date: string 'yymmdd' indicating the date
272 - time: string 'hhmmss' indicating the time
273 Return:
274 - resp: server response if successful
275 - list: list of newsgroup names"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000276
Guido van Rossuma2685402003-04-19 18:04:57 +0000277 return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000278
Guido van Rossuma2685402003-04-19 18:04:57 +0000279 def newnews(self, group, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000280 """Process a NEWNEWS command. Arguments:
281 - group: group name or '*'
282 - date: string 'yymmdd' indicating the date
283 - time: string 'hhmmss' indicating the time
284 Return:
285 - resp: server response if successful
Georg Brandl5dbda752005-07-17 20:27:41 +0000286 - list: list of message ids"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000287
Tim Peters2344fae2001-01-15 00:50:52 +0000288 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
Guido van Rossuma2685402003-04-19 18:04:57 +0000289 return self.longcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000290
Guido van Rossuma2685402003-04-19 18:04:57 +0000291 def list(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000292 """Process a LIST command. Return:
293 - resp: server response if successful
294 - list: list of (group, last, first, flag) (strings)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000295
Guido van Rossuma2685402003-04-19 18:04:57 +0000296 resp, list = self.longcmd('LIST', file)
Tim Peters2344fae2001-01-15 00:50:52 +0000297 for i in range(len(list)):
298 # Parse lines into "group last first flag"
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000299 list[i] = tuple(list[i].split())
Tim Peters2344fae2001-01-15 00:50:52 +0000300 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000301
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000302 def description(self, group):
303
304 """Get a description for a single group. If more than one
305 group matches ('group' is a pattern), return the first. If no
306 group matches, return an empty string.
307
308 This elides the response code from the server, since it can
309 only be '215' or '285' (for xgtitle) anyway. If the response
310 code is needed, use the 'descriptions' method.
311
312 NOTE: This neither checks for a wildcard in 'group' nor does
313 it check whether the group actually exists."""
314
315 resp, lines = self.descriptions(group)
316 if len(lines) == 0:
Christian Heimes933238a2008-11-05 19:44:21 +0000317 return b''
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000318 else:
319 return lines[0][1]
320
321 def descriptions(self, group_pattern):
322 """Get descriptions for a range of groups."""
Christian Heimes933238a2008-11-05 19:44:21 +0000323 line_pat = re.compile(b'^(?P<group>[^ \t]+)[ \t]+(.*)$')
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000324 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
325 resp, raw_lines = self.longcmd('LIST NEWSGROUPS ' + group_pattern)
Christian Heimes933238a2008-11-05 19:44:21 +0000326 if not resp.startswith(b'215'):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000327 # Now the deprecated XGTITLE. This either raises an error
328 # or succeeds with the same output structure as LIST
329 # NEWSGROUPS.
330 resp, raw_lines = self.longcmd('XGTITLE ' + group_pattern)
331 lines = []
332 for raw_line in raw_lines:
333 match = line_pat.search(raw_line.strip())
334 if match:
335 lines.append(match.group(1, 2))
336 return resp, lines
337
Tim Peters2344fae2001-01-15 00:50:52 +0000338 def group(self, name):
339 """Process a GROUP command. Argument:
340 - group: the group name
341 Returns:
342 - resp: server response if successful
343 - count: number of articles (string)
344 - first: first article number (string)
345 - last: last article number (string)
346 - name: the group name"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000347
Tim Peters2344fae2001-01-15 00:50:52 +0000348 resp = self.shortcmd('GROUP ' + name)
Christian Heimes933238a2008-11-05 19:44:21 +0000349 if not resp.startswith(b'211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000350 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000351 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000352 count = first = last = 0
353 n = len(words)
354 if n > 1:
355 count = words[1]
356 if n > 2:
357 first = words[2]
358 if n > 3:
359 last = words[3]
360 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000361 name = words[4].lower()
Tim Peters2344fae2001-01-15 00:50:52 +0000362 return resp, count, first, last, name
Guido van Rossumc629d341992-11-05 10:43:02 +0000363
Guido van Rossuma2685402003-04-19 18:04:57 +0000364 def help(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000365 """Process a HELP command. Returns:
366 - resp: server response if successful
367 - list: list of strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000368
Guido van Rossuma2685402003-04-19 18:04:57 +0000369 return self.longcmd('HELP',file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000370
Tim Peters2344fae2001-01-15 00:50:52 +0000371 def statparse(self, resp):
372 """Internal: parse the response of a STAT, NEXT or LAST command."""
Christian Heimes933238a2008-11-05 19:44:21 +0000373 if not resp.startswith(b'22'):
Tim Peters2344fae2001-01-15 00:50:52 +0000374 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000375 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000376 nr = 0
Christian Heimes933238a2008-11-05 19:44:21 +0000377 id = b''
Tim Peters2344fae2001-01-15 00:50:52 +0000378 n = len(words)
379 if n > 1:
380 nr = words[1]
381 if n > 2:
382 id = words[2]
383 return resp, nr, id
Guido van Rossumc629d341992-11-05 10:43:02 +0000384
Tim Peters2344fae2001-01-15 00:50:52 +0000385 def statcmd(self, line):
386 """Internal: process a STAT, NEXT or LAST command."""
387 resp = self.shortcmd(line)
388 return self.statparse(resp)
Guido van Rossumc629d341992-11-05 10:43:02 +0000389
Tim Peters2344fae2001-01-15 00:50:52 +0000390 def stat(self, id):
391 """Process a STAT command. Argument:
392 - id: article number or message id
393 Returns:
394 - resp: server response if successful
395 - nr: the article number
Georg Brandl5dbda752005-07-17 20:27:41 +0000396 - id: the message id"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000397
Christian Heimes933238a2008-11-05 19:44:21 +0000398 return self.statcmd('STAT {0}'.format(id))
Guido van Rossumc629d341992-11-05 10:43:02 +0000399
Tim Peters2344fae2001-01-15 00:50:52 +0000400 def next(self):
401 """Process a NEXT command. No arguments. Return as for STAT."""
402 return self.statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000403
Tim Peters2344fae2001-01-15 00:50:52 +0000404 def last(self):
405 """Process a LAST command. No arguments. Return as for STAT."""
406 return self.statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000407
Fredrik Lundha5e61652001-10-18 20:58:25 +0000408 def artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000409 """Internal: process a HEAD, BODY or ARTICLE command."""
Fredrik Lundha5e61652001-10-18 20:58:25 +0000410 resp, list = self.longcmd(line, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000411 resp, nr, id = self.statparse(resp)
412 return resp, nr, id, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000413
Tim Peters2344fae2001-01-15 00:50:52 +0000414 def head(self, id):
415 """Process a HEAD command. Argument:
416 - id: article number or message id
417 Returns:
418 - resp: server response if successful
419 - nr: article number
420 - id: message id
421 - list: the lines of the article's header"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000422
Christian Heimes933238a2008-11-05 19:44:21 +0000423 return self.artcmd('HEAD {0}'.format(id))
Guido van Rossumc629d341992-11-05 10:43:02 +0000424
Fredrik Lundha5e61652001-10-18 20:58:25 +0000425 def body(self, id, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000426 """Process a BODY command. Argument:
427 - id: article number or message id
Fredrik Lundha5e61652001-10-18 20:58:25 +0000428 - file: Filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000429 Returns:
430 - resp: server response if successful
431 - nr: article number
432 - id: message id
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000433 - list: the lines of the article's body or an empty list
Fredrik Lundha5e61652001-10-18 20:58:25 +0000434 if file was used"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000435
Christian Heimes933238a2008-11-05 19:44:21 +0000436 return self.artcmd('BODY {0}'.format(id), file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000437
Tim Peters2344fae2001-01-15 00:50:52 +0000438 def article(self, id):
439 """Process an ARTICLE command. Argument:
440 - id: article number or message id
441 Returns:
442 - resp: server response if successful
443 - nr: article number
444 - id: message id
445 - list: the lines of the article"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000446
Christian Heimes933238a2008-11-05 19:44:21 +0000447 return self.artcmd('ARTICLE {0}'.format(id))
Guido van Rossumc629d341992-11-05 10:43:02 +0000448
Tim Peters2344fae2001-01-15 00:50:52 +0000449 def slave(self):
450 """Process a SLAVE command. Returns:
451 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000452
Tim Peters2344fae2001-01-15 00:50:52 +0000453 return self.shortcmd('SLAVE')
Guido van Rossumc629d341992-11-05 10:43:02 +0000454
Guido van Rossuma2685402003-04-19 18:04:57 +0000455 def xhdr(self, hdr, str, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000456 """Process an XHDR command (optional server extension). Arguments:
457 - hdr: the header type (e.g. 'subject')
458 - str: an article nr, a message id, or a range nr1-nr2
459 Returns:
460 - resp: server response if successful
461 - list: list of (nr, value) strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000462
Christian Heimes933238a2008-11-05 19:44:21 +0000463 pat = re.compile(b'^([0-9]+) ?(.*)\n?')
464 resp, lines = self.longcmd('XHDR {0} {1}'.format(hdr, str), file)
Tim Peters2344fae2001-01-15 00:50:52 +0000465 for i in range(len(lines)):
466 line = lines[i]
467 m = pat.match(line)
468 if m:
469 lines[i] = m.group(1, 2)
470 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000471
Guido van Rossuma2685402003-04-19 18:04:57 +0000472 def xover(self, start, end, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000473 """Process an XOVER command (optional server extension) Arguments:
474 - start: start of range
475 - end: end of range
476 Returns:
477 - resp: server response if successful
478 - list: list of (art-nr, subject, poster, date,
479 id, references, size, lines)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000480
Christian Heimes933238a2008-11-05 19:44:21 +0000481 resp, lines = self.longcmd('XOVER {0}-{1}'.format(start, end), file)
Tim Peters2344fae2001-01-15 00:50:52 +0000482 xover_lines = []
483 for line in lines:
Christian Heimes933238a2008-11-05 19:44:21 +0000484 elem = line.split(b'\t')
Tim Peters2344fae2001-01-15 00:50:52 +0000485 try:
486 xover_lines.append((elem[0],
487 elem[1],
488 elem[2],
489 elem[3],
490 elem[4],
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000491 elem[5].split(),
Tim Peters2344fae2001-01-15 00:50:52 +0000492 elem[6],
493 elem[7]))
494 except IndexError:
495 raise NNTPDataError(line)
496 return resp,xover_lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000497
Guido van Rossuma2685402003-04-19 18:04:57 +0000498 def xgtitle(self, group, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000499 """Process an XGTITLE command (optional server extension) Arguments:
500 - group: group name wildcard (i.e. news.*)
501 Returns:
502 - resp: server response if successful
503 - list: list of (name,title) strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000504
Christian Heimes933238a2008-11-05 19:44:21 +0000505 line_pat = re.compile(b'^([^ \t]+)[ \t]+(.*)$')
Guido van Rossuma2685402003-04-19 18:04:57 +0000506 resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000507 lines = []
508 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000509 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000510 if match:
511 lines.append(match.group(1, 2))
512 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000513
Tim Peters2344fae2001-01-15 00:50:52 +0000514 def xpath(self,id):
515 """Process an XPATH command (optional server extension) Arguments:
516 - id: Message id of article
517 Returns:
518 resp: server response if successful
519 path: directory path to article"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000520
Christian Heimes933238a2008-11-05 19:44:21 +0000521 resp = self.shortcmd('XPATH {0}'.format(id))
522 if not resp.startswith(b'223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000523 raise NNTPReplyError(resp)
524 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000525 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000526 except ValueError:
527 raise NNTPReplyError(resp)
528 else:
529 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000530
Tim Peters2344fae2001-01-15 00:50:52 +0000531 def date (self):
532 """Process the DATE command. Arguments:
533 None
534 Returns:
535 resp: server response if successful
536 date: Date suitable for newnews/newgroups commands etc.
537 time: Time suitable for newnews/newgroups commands etc."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000538
Tim Peters2344fae2001-01-15 00:50:52 +0000539 resp = self.shortcmd("DATE")
Christian Heimes933238a2008-11-05 19:44:21 +0000540 if not resp.startswith(b'111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000541 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000542 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000543 if len(elem) != 2:
544 raise NNTPDataError(resp)
545 date = elem[1][2:8]
546 time = elem[1][-6:]
547 if len(date) != 6 or len(time) != 6:
548 raise NNTPDataError(resp)
549 return resp, date, time
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000550
Christian Heimes933238a2008-11-05 19:44:21 +0000551 def _post(self, command, f):
552 resp = self.shortcmd(command)
553 # Raises error_??? if posting is not allowed
554 if not resp.startswith(b'3'):
555 raise NNTPReplyError(resp)
556 while 1:
557 line = f.readline()
558 if not line:
559 break
560 if line.endswith(b'\n'):
561 line = line[:-1]
562 if line.startswith(b'.'):
563 line = b'.' + line
564 self.putline(line)
565 self.putline(b'.')
566 return self.getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000567
Tim Peters2344fae2001-01-15 00:50:52 +0000568 def post(self, f):
569 """Process a POST command. Arguments:
570 - f: file containing the article
571 Returns:
572 - resp: server response if successful"""
Christian Heimes933238a2008-11-05 19:44:21 +0000573 return self._post('POST', f)
Guido van Rossumc629d341992-11-05 10:43:02 +0000574
Tim Peters2344fae2001-01-15 00:50:52 +0000575 def ihave(self, id, f):
576 """Process an IHAVE command. Arguments:
577 - id: message-id of the article
578 - f: file containing the article
579 Returns:
580 - resp: server response if successful
581 Note that if the server refuses the article an exception is raised."""
Christian Heimes933238a2008-11-05 19:44:21 +0000582 return self._post('IHAVE {0}'.format(id), f)
Guido van Rossumc629d341992-11-05 10:43:02 +0000583
Tim Peters2344fae2001-01-15 00:50:52 +0000584 def quit(self):
585 """Process a QUIT command and close the socket. Returns:
586 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000587
Tim Peters2344fae2001-01-15 00:50:52 +0000588 resp = self.shortcmd('QUIT')
589 self.file.close()
590 self.sock.close()
591 del self.file, self.sock
592 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000593
594
Neal Norwitzef679562002-11-14 02:19:44 +0000595# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000596# Assumption: if there's a local news server, it's called 'news'.
597# Assumption: if user queries a remote news server, it's named
598# in the environment variable NNTPSERVER (used by slrn and kin)
599# and we want readermode off.
600if __name__ == '__main__':
601 import os
602 newshost = 'news' and os.environ["NNTPSERVER"]
603 if newshost.find('.') == -1:
604 mode = 'readermode'
605 else:
606 mode = None
607 s = NNTP(newshost, readermode=mode)
Tim Peters2344fae2001-01-15 00:50:52 +0000608 resp, count, first, last, name = s.group('comp.lang.python')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000609 print(resp)
610 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Christian Heimes933238a2008-11-05 19:44:21 +0000611 resp, subs = s.xhdr('subject', '{0}-{1}'.format(first, last))
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000612 print(resp)
Tim Peters2344fae2001-01-15 00:50:52 +0000613 for item in subs:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000614 print("%7s %s" % item)
Tim Peters2344fae2001-01-15 00:50:52 +0000615 resp = s.quit()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000616 print(resp)