blob: 4d7f9fd8cb698da78c714895c9246b9cd614f790 [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
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
Guido van Rossumb940e112007-01-10 16:19:56 +0000130 except NNTPTemporaryError as e:
Christian Heimes933238a2008-11-05 19:44:21 +0000131 if user and e.response.startswith(b'480'):
Thomas Wouters47adcba2001-01-16 06:35:14 +0000132 # 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)
Christian Heimes933238a2008-11-05 19:44:21 +0000151 if resp.startswith(b'381'):
Tim Peters2344fae2001-01-15 00:50:52 +0000152 if not password:
153 raise NNTPReplyError(resp)
154 else:
155 resp = self.shortcmd(
156 'authinfo pass '+password)
Christian Heimes933238a2008-11-05 19:44:21 +0000157 if not resp.startswith(b'281'):
Tim Peters2344fae2001-01-15 00:50:52 +0000158 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
Guido van Rossumbe19ed72007-02-09 05:37:30 +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
Guido van Rossumbe19ed72007-02-09 05:37:30 +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())."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000198 if self.debugging: print('*cmd*', repr(line))
Christian Heimes933238a2008-11-05 19:44:21 +0000199 line = bytes(line, "ASCII")
Tim Peters2344fae2001-01-15 00:50:52 +0000200 self.putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000201
Tim Peters2344fae2001-01-15 00:50:52 +0000202 def getline(self):
203 """Internal: return one line from the server, stripping CRLF.
204 Raise EOFError if the connection is closed."""
205 line = self.file.readline()
206 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000207 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000208 if not line: raise EOFError
Christian Heimes933238a2008-11-05 19:44:21 +0000209 if line[-2:] == CRLF:
210 line = line[:-2]
211 elif line[-1:] in CRLF:
212 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000213 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000214
Tim Peters2344fae2001-01-15 00:50:52 +0000215 def getresp(self):
216 """Internal: get a response from the server.
217 Raise various errors if the response indicates an error."""
218 resp = self.getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000219 if self.debugging: print('*resp*', repr(resp))
Tim Peters2344fae2001-01-15 00:50:52 +0000220 c = resp[:1]
Christian Heimes933238a2008-11-05 19:44:21 +0000221 if c == b'4':
Tim Peters2344fae2001-01-15 00:50:52 +0000222 raise NNTPTemporaryError(resp)
Christian Heimes933238a2008-11-05 19:44:21 +0000223 if c == b'5':
Tim Peters2344fae2001-01-15 00:50:52 +0000224 raise NNTPPermanentError(resp)
Christian Heimes933238a2008-11-05 19:44:21 +0000225 if c not in b'123':
Tim Peters2344fae2001-01-15 00:50:52 +0000226 raise NNTPProtocolError(resp)
227 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000228
Fredrik Lundha5e61652001-10-18 20:58:25 +0000229 def getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000230 """Internal: get a response plus following text from the server.
231 Raise various errors if the response indicates an error."""
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000232
233 openedFile = None
234 try:
235 # If a string was passed then open a file with that name
Walter Dörwald65230a22002-06-03 15:58:32 +0000236 if isinstance(file, str):
Fredrik Lundha5e61652001-10-18 20:58:25 +0000237 openedFile = file = open(file, "w")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000238
239 resp = self.getresp()
240 if resp[:3] not in LONGRESP:
241 raise NNTPReplyError(resp)
242 list = []
243 while 1:
244 line = self.getline()
Christian Heimes933238a2008-11-05 19:44:21 +0000245 if line == b'.':
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000246 break
Christian Heimes933238a2008-11-05 19:44:21 +0000247 if line.startswith(b'..'):
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000248 line = line[1:]
Fredrik Lundha5e61652001-10-18 20:58:25 +0000249 if file:
Christian Heimes933238a2008-11-05 19:44:21 +0000250 file.write(line + b'\n')
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000251 else:
252 list.append(line)
253 finally:
254 # If this method created the file, then it must close it
255 if openedFile:
256 openedFile.close()
257
Tim Peters2344fae2001-01-15 00:50:52 +0000258 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000259
Tim Peters2344fae2001-01-15 00:50:52 +0000260 def shortcmd(self, line):
261 """Internal: send a command and get the response."""
262 self.putcmd(line)
263 return self.getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000264
Fredrik Lundha5e61652001-10-18 20:58:25 +0000265 def longcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000266 """Internal: send a command and get the response plus following text."""
267 self.putcmd(line)
Fredrik Lundha5e61652001-10-18 20:58:25 +0000268 return self.getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000269
Guido van Rossuma2685402003-04-19 18:04:57 +0000270 def newgroups(self, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000271 """Process a NEWGROUPS command. Arguments:
272 - date: string 'yymmdd' indicating the date
273 - time: string 'hhmmss' indicating the time
274 Return:
275 - resp: server response if successful
276 - list: list of newsgroup names"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000277
Guido van Rossuma2685402003-04-19 18:04:57 +0000278 return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000279
Guido van Rossuma2685402003-04-19 18:04:57 +0000280 def newnews(self, group, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000281 """Process a NEWNEWS command. Arguments:
282 - group: group name or '*'
283 - date: string 'yymmdd' indicating the date
284 - time: string 'hhmmss' indicating the time
285 Return:
286 - resp: server response if successful
Georg Brandl5dbda752005-07-17 20:27:41 +0000287 - list: list of message ids"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000288
Tim Peters2344fae2001-01-15 00:50:52 +0000289 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
Guido van Rossuma2685402003-04-19 18:04:57 +0000290 return self.longcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000291
Guido van Rossuma2685402003-04-19 18:04:57 +0000292 def list(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000293 """Process a LIST command. Return:
294 - resp: server response if successful
295 - list: list of (group, last, first, flag) (strings)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000296
Guido van Rossuma2685402003-04-19 18:04:57 +0000297 resp, list = self.longcmd('LIST', file)
Tim Peters2344fae2001-01-15 00:50:52 +0000298 for i in range(len(list)):
299 # Parse lines into "group last first flag"
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000300 list[i] = tuple(list[i].split())
Tim Peters2344fae2001-01-15 00:50:52 +0000301 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000302
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000303 def description(self, group):
304
305 """Get a description for a single group. If more than one
306 group matches ('group' is a pattern), return the first. If no
307 group matches, return an empty string.
308
309 This elides the response code from the server, since it can
310 only be '215' or '285' (for xgtitle) anyway. If the response
311 code is needed, use the 'descriptions' method.
312
313 NOTE: This neither checks for a wildcard in 'group' nor does
314 it check whether the group actually exists."""
315
316 resp, lines = self.descriptions(group)
317 if len(lines) == 0:
Christian Heimes933238a2008-11-05 19:44:21 +0000318 return b''
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000319 else:
320 return lines[0][1]
321
322 def descriptions(self, group_pattern):
323 """Get descriptions for a range of groups."""
Christian Heimes933238a2008-11-05 19:44:21 +0000324 line_pat = re.compile(b'^(?P<group>[^ \t]+)[ \t]+(.*)$')
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000325 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
326 resp, raw_lines = self.longcmd('LIST NEWSGROUPS ' + group_pattern)
Christian Heimes933238a2008-11-05 19:44:21 +0000327 if not resp.startswith(b'215'):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000328 # Now the deprecated XGTITLE. This either raises an error
329 # or succeeds with the same output structure as LIST
330 # NEWSGROUPS.
331 resp, raw_lines = self.longcmd('XGTITLE ' + group_pattern)
332 lines = []
333 for raw_line in raw_lines:
334 match = line_pat.search(raw_line.strip())
335 if match:
336 lines.append(match.group(1, 2))
337 return resp, lines
338
Tim Peters2344fae2001-01-15 00:50:52 +0000339 def group(self, name):
340 """Process a GROUP command. Argument:
341 - group: the group name
342 Returns:
343 - resp: server response if successful
344 - count: number of articles (string)
345 - first: first article number (string)
346 - last: last article number (string)
347 - name: the group name"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000348
Tim Peters2344fae2001-01-15 00:50:52 +0000349 resp = self.shortcmd('GROUP ' + name)
Christian Heimes933238a2008-11-05 19:44:21 +0000350 if not resp.startswith(b'211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000351 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000352 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000353 count = first = last = 0
354 n = len(words)
355 if n > 1:
356 count = words[1]
357 if n > 2:
358 first = words[2]
359 if n > 3:
360 last = words[3]
361 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000362 name = words[4].lower()
Tim Peters2344fae2001-01-15 00:50:52 +0000363 return resp, count, first, last, name
Guido van Rossumc629d341992-11-05 10:43:02 +0000364
Guido van Rossuma2685402003-04-19 18:04:57 +0000365 def help(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000366 """Process a HELP command. Returns:
367 - resp: server response if successful
368 - list: list of strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000369
Guido van Rossuma2685402003-04-19 18:04:57 +0000370 return self.longcmd('HELP',file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000371
Tim Peters2344fae2001-01-15 00:50:52 +0000372 def statparse(self, resp):
373 """Internal: parse the response of a STAT, NEXT or LAST command."""
Christian Heimes933238a2008-11-05 19:44:21 +0000374 if not resp.startswith(b'22'):
Tim Peters2344fae2001-01-15 00:50:52 +0000375 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000376 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000377 nr = 0
Christian Heimes933238a2008-11-05 19:44:21 +0000378 id = b''
Tim Peters2344fae2001-01-15 00:50:52 +0000379 n = len(words)
380 if n > 1:
381 nr = words[1]
382 if n > 2:
383 id = words[2]
384 return resp, nr, id
Guido van Rossumc629d341992-11-05 10:43:02 +0000385
Tim Peters2344fae2001-01-15 00:50:52 +0000386 def statcmd(self, line):
387 """Internal: process a STAT, NEXT or LAST command."""
388 resp = self.shortcmd(line)
389 return self.statparse(resp)
Guido van Rossumc629d341992-11-05 10:43:02 +0000390
Tim Peters2344fae2001-01-15 00:50:52 +0000391 def stat(self, id):
392 """Process a STAT command. Argument:
393 - id: article number or message id
394 Returns:
395 - resp: server response if successful
396 - nr: the article number
Georg Brandl5dbda752005-07-17 20:27:41 +0000397 - id: the message id"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000398
Christian Heimes933238a2008-11-05 19:44:21 +0000399 return self.statcmd('STAT {0}'.format(id))
Guido van Rossumc629d341992-11-05 10:43:02 +0000400
Tim Peters2344fae2001-01-15 00:50:52 +0000401 def next(self):
402 """Process a NEXT command. No arguments. Return as for STAT."""
403 return self.statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000404
Tim Peters2344fae2001-01-15 00:50:52 +0000405 def last(self):
406 """Process a LAST command. No arguments. Return as for STAT."""
407 return self.statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000408
Fredrik Lundha5e61652001-10-18 20:58:25 +0000409 def artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000410 """Internal: process a HEAD, BODY or ARTICLE command."""
Fredrik Lundha5e61652001-10-18 20:58:25 +0000411 resp, list = self.longcmd(line, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000412 resp, nr, id = self.statparse(resp)
413 return resp, nr, id, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000414
Tim Peters2344fae2001-01-15 00:50:52 +0000415 def head(self, id):
416 """Process a HEAD command. Argument:
417 - id: article number or message id
418 Returns:
419 - resp: server response if successful
420 - nr: article number
421 - id: message id
422 - list: the lines of the article's header"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000423
Christian Heimes933238a2008-11-05 19:44:21 +0000424 return self.artcmd('HEAD {0}'.format(id))
Guido van Rossumc629d341992-11-05 10:43:02 +0000425
Fredrik Lundha5e61652001-10-18 20:58:25 +0000426 def body(self, id, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000427 """Process a BODY command. Argument:
428 - id: article number or message id
Fredrik Lundha5e61652001-10-18 20:58:25 +0000429 - file: Filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000430 Returns:
431 - resp: server response if successful
432 - nr: article number
433 - id: message id
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000434 - list: the lines of the article's body or an empty list
Fredrik Lundha5e61652001-10-18 20:58:25 +0000435 if file was used"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000436
Christian Heimes933238a2008-11-05 19:44:21 +0000437 return self.artcmd('BODY {0}'.format(id), file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000438
Tim Peters2344fae2001-01-15 00:50:52 +0000439 def article(self, id):
440 """Process an ARTICLE command. Argument:
441 - id: article number or message id
442 Returns:
443 - resp: server response if successful
444 - nr: article number
445 - id: message id
446 - list: the lines of the article"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000447
Christian Heimes933238a2008-11-05 19:44:21 +0000448 return self.artcmd('ARTICLE {0}'.format(id))
Guido van Rossumc629d341992-11-05 10:43:02 +0000449
Tim Peters2344fae2001-01-15 00:50:52 +0000450 def slave(self):
451 """Process a SLAVE command. Returns:
452 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000453
Tim Peters2344fae2001-01-15 00:50:52 +0000454 return self.shortcmd('SLAVE')
Guido van Rossumc629d341992-11-05 10:43:02 +0000455
Guido van Rossuma2685402003-04-19 18:04:57 +0000456 def xhdr(self, hdr, str, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000457 """Process an XHDR command (optional server extension). Arguments:
458 - hdr: the header type (e.g. 'subject')
459 - str: an article nr, a message id, or a range nr1-nr2
460 Returns:
461 - resp: server response if successful
462 - list: list of (nr, value) strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000463
Christian Heimes933238a2008-11-05 19:44:21 +0000464 pat = re.compile(b'^([0-9]+) ?(.*)\n?')
465 resp, lines = self.longcmd('XHDR {0} {1}'.format(hdr, str), file)
Tim Peters2344fae2001-01-15 00:50:52 +0000466 for i in range(len(lines)):
467 line = lines[i]
468 m = pat.match(line)
469 if m:
470 lines[i] = m.group(1, 2)
471 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000472
Guido van Rossuma2685402003-04-19 18:04:57 +0000473 def xover(self, start, end, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000474 """Process an XOVER command (optional server extension) Arguments:
475 - start: start of range
476 - end: end of range
477 Returns:
478 - resp: server response if successful
479 - list: list of (art-nr, subject, poster, date,
480 id, references, size, lines)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000481
Christian Heimes933238a2008-11-05 19:44:21 +0000482 resp, lines = self.longcmd('XOVER {0}-{1}'.format(start, end), file)
Tim Peters2344fae2001-01-15 00:50:52 +0000483 xover_lines = []
484 for line in lines:
Christian Heimes933238a2008-11-05 19:44:21 +0000485 elem = line.split(b'\t')
Tim Peters2344fae2001-01-15 00:50:52 +0000486 try:
487 xover_lines.append((elem[0],
488 elem[1],
489 elem[2],
490 elem[3],
491 elem[4],
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000492 elem[5].split(),
Tim Peters2344fae2001-01-15 00:50:52 +0000493 elem[6],
494 elem[7]))
495 except IndexError:
496 raise NNTPDataError(line)
497 return resp,xover_lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000498
Guido van Rossuma2685402003-04-19 18:04:57 +0000499 def xgtitle(self, group, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000500 """Process an XGTITLE command (optional server extension) Arguments:
501 - group: group name wildcard (i.e. news.*)
502 Returns:
503 - resp: server response if successful
504 - list: list of (name,title) strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000505
Christian Heimes933238a2008-11-05 19:44:21 +0000506 line_pat = re.compile(b'^([^ \t]+)[ \t]+(.*)$')
Guido van Rossuma2685402003-04-19 18:04:57 +0000507 resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000508 lines = []
509 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000510 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000511 if match:
512 lines.append(match.group(1, 2))
513 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000514
Tim Peters2344fae2001-01-15 00:50:52 +0000515 def xpath(self,id):
516 """Process an XPATH command (optional server extension) Arguments:
517 - id: Message id of article
518 Returns:
519 resp: server response if successful
520 path: directory path to article"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000521
Christian Heimes933238a2008-11-05 19:44:21 +0000522 resp = self.shortcmd('XPATH {0}'.format(id))
523 if not resp.startswith(b'223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000524 raise NNTPReplyError(resp)
525 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000526 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000527 except ValueError:
528 raise NNTPReplyError(resp)
529 else:
530 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000531
Tim Peters2344fae2001-01-15 00:50:52 +0000532 def date (self):
533 """Process the DATE command. Arguments:
534 None
535 Returns:
536 resp: server response if successful
537 date: Date suitable for newnews/newgroups commands etc.
538 time: Time suitable for newnews/newgroups commands etc."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000539
Tim Peters2344fae2001-01-15 00:50:52 +0000540 resp = self.shortcmd("DATE")
Christian Heimes933238a2008-11-05 19:44:21 +0000541 if not resp.startswith(b'111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000542 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000543 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000544 if len(elem) != 2:
545 raise NNTPDataError(resp)
546 date = elem[1][2:8]
547 time = elem[1][-6:]
548 if len(date) != 6 or len(time) != 6:
549 raise NNTPDataError(resp)
550 return resp, date, time
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000551
Christian Heimes933238a2008-11-05 19:44:21 +0000552 def _post(self, command, f):
553 resp = self.shortcmd(command)
554 # Raises error_??? if posting is not allowed
555 if not resp.startswith(b'3'):
556 raise NNTPReplyError(resp)
557 while 1:
558 line = f.readline()
559 if not line:
560 break
561 if line.endswith(b'\n'):
562 line = line[:-1]
563 if line.startswith(b'.'):
564 line = b'.' + line
565 self.putline(line)
566 self.putline(b'.')
567 return self.getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000568
Tim Peters2344fae2001-01-15 00:50:52 +0000569 def post(self, f):
570 """Process a POST command. Arguments:
571 - f: file containing the article
572 Returns:
573 - resp: server response if successful"""
Christian Heimes933238a2008-11-05 19:44:21 +0000574 return self._post('POST', f)
Guido van Rossumc629d341992-11-05 10:43:02 +0000575
Tim Peters2344fae2001-01-15 00:50:52 +0000576 def ihave(self, id, f):
577 """Process an IHAVE command. Arguments:
578 - id: message-id of the article
579 - f: file containing the article
580 Returns:
581 - resp: server response if successful
582 Note that if the server refuses the article an exception is raised."""
Christian Heimes933238a2008-11-05 19:44:21 +0000583 return self._post('IHAVE {0}'.format(id), f)
Guido van Rossumc629d341992-11-05 10:43:02 +0000584
Tim Peters2344fae2001-01-15 00:50:52 +0000585 def quit(self):
586 """Process a QUIT command and close the socket. Returns:
587 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000588
Tim Peters2344fae2001-01-15 00:50:52 +0000589 resp = self.shortcmd('QUIT')
590 self.file.close()
591 self.sock.close()
592 del self.file, self.sock
593 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000594
595
Neal Norwitzef679562002-11-14 02:19:44 +0000596# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000597# Assumption: if there's a local news server, it's called 'news'.
598# Assumption: if user queries a remote news server, it's named
599# in the environment variable NNTPSERVER (used by slrn and kin)
600# and we want readermode off.
601if __name__ == '__main__':
602 import os
603 newshost = 'news' and os.environ["NNTPSERVER"]
604 if newshost.find('.') == -1:
605 mode = 'readermode'
606 else:
607 mode = None
608 s = NNTP(newshost, readermode=mode)
Tim Peters2344fae2001-01-15 00:50:52 +0000609 resp, count, first, last, name = s.group('comp.lang.python')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000610 print(resp)
611 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Christian Heimes933238a2008-11-05 19:44:21 +0000612 resp, subs = s.xhdr('subject', '{0}-{1}'.format(first, last))
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000613 print(resp)
Tim Peters2344fae2001-01-15 00:50:52 +0000614 for item in subs:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000615 print("%7s %s" % item)
Tim Peters2344fae2001-01-15 00:50:52 +0000616 resp = s.quit()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000617 print(resp)