blob: 81ebe4bc035787ab85ad9e36f7a90e99aad5d00e [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 Warsaw42faa552013-09-30 18:35:15 -040040# maximal line length when calling readline(). This is to prevent
Barry Warsawaa914502013-10-01 11:38:38 -040041# reading arbitrary length lines. RFC 3977 limits NNTP line length to
Barry Warsaw42faa552013-09-30 18:35:15 -040042# 512 characters, including CRLF. We have selected 2048 just to be on
43# the safe side.
44_MAXLINE = 2048
45
46
Barry Warsaw9dd78722000-02-10 20:25:53 +000047# Exceptions raised when an error or invalid response is received
48class NNTPError(Exception):
Tim Peters2344fae2001-01-15 00:50:52 +000049 """Base class for all nntplib exceptions"""
50 def __init__(self, *args):
Guido van Rossum68468eb2003-02-27 20:14:51 +000051 Exception.__init__(self, *args)
Tim Peters2344fae2001-01-15 00:50:52 +000052 try:
53 self.response = args[0]
54 except IndexError:
55 self.response = 'No response given'
Barry Warsaw9dd78722000-02-10 20:25:53 +000056
57class NNTPReplyError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000058 """Unexpected [123]xx reply"""
59 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000060
61class NNTPTemporaryError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000062 """4xx errors"""
63 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000064
65class NNTPPermanentError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000066 """5xx errors"""
67 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000068
69class NNTPProtocolError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000070 """Response does not begin with [1-5]"""
71 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000072
73class NNTPDataError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000074 """Error in response data"""
75 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000076
77# for backwards compatibility
78error_reply = NNTPReplyError
79error_temp = NNTPTemporaryError
80error_perm = NNTPPermanentError
81error_proto = NNTPProtocolError
82error_data = NNTPDataError
Guido van Rossumc629d341992-11-05 10:43:02 +000083
84
Tim Peters2344fae2001-01-15 00:50:52 +000085
Guido van Rossumc629d341992-11-05 10:43:02 +000086# Standard port used by NNTP servers
87NNTP_PORT = 119
88
89
90# Response numbers that are followed by additional text (e.g. article)
Guido van Rossum8421c4e1995-09-22 00:52:38 +000091LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
Guido van Rossumc629d341992-11-05 10:43:02 +000092
93
94# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
95CRLF = '\r\n'
96
97
Tim Peters2344fae2001-01-15 00:50:52 +000098
Guido van Rossumc629d341992-11-05 10:43:02 +000099# The class itself
Guido van Rossumc629d341992-11-05 10:43:02 +0000100class NNTP:
Tim Peters2344fae2001-01-15 00:50:52 +0000101 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
Martin v. Löwis9513e342004-08-03 14:36:32 +0000102 readermode=None, usenetrc=True):
Tim Peters2344fae2001-01-15 00:50:52 +0000103 """Initialize an instance. Arguments:
104 - host: hostname to connect to
105 - port: port to connect to (default the standard NNTP port)
106 - user: username to authenticate with
107 - password: password to use with username
108 - readermode: if true, send 'mode reader' command after
109 connecting.
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000110
Tim Peters2344fae2001-01-15 00:50:52 +0000111 readermode is sometimes necessary if you are connecting to an
112 NNTP server on the local machine and intend to call
Ezio Melotti24b07bc2011-03-15 18:55:01 +0200113 reader-specific commands, such as `group'. If you get
Tim Peters2344fae2001-01-15 00:50:52 +0000114 unexpected NNTPPermanentErrors, you might need to set
115 readermode.
116 """
117 self.host = host
118 self.port = port
Antoine Pitroueed30d82009-05-14 21:22:08 +0000119 self.sock = socket.create_connection((host, port))
Tim Peters2344fae2001-01-15 00:50:52 +0000120 self.file = self.sock.makefile('rb')
121 self.debugging = 0
122 self.welcome = self.getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000123
Thomas Wouters47adcba2001-01-16 06:35:14 +0000124 # 'mode reader' is sometimes necessary to enable 'reader' mode.
Tim Petersdfb673b2001-01-16 07:12:46 +0000125 # However, the order in which 'mode reader' and 'authinfo' need to
Thomas Wouters47adcba2001-01-16 06:35:14 +0000126 # arrive differs between some NNTP servers. Try to send
127 # 'mode reader', and if it fails with an authorization failed
128 # error, try again after sending authinfo.
129 readermode_afterauth = 0
Tim Peters2344fae2001-01-15 00:50:52 +0000130 if readermode:
131 try:
132 self.welcome = self.shortcmd('mode reader')
133 except NNTPPermanentError:
134 # error 500, probably 'not implemented'
135 pass
Thomas Wouters47adcba2001-01-16 06:35:14 +0000136 except NNTPTemporaryError, e:
137 if user and e.response[:3] == '480':
138 # Need authorization before 'mode reader'
139 readermode_afterauth = 1
140 else:
141 raise
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000142 # If no login/password was specified, try to get them from ~/.netrc
143 # Presume that if .netc has an entry, NNRP authentication is required.
Eric S. Raymond782d9402002-11-17 17:53:12 +0000144 try:
Martin v. Löwis9513e342004-08-03 14:36:32 +0000145 if usenetrc and not user:
Eric S. Raymond782d9402002-11-17 17:53:12 +0000146 import netrc
147 credentials = netrc.netrc()
148 auth = credentials.authenticators(host)
149 if auth:
150 user = auth[0]
151 password = auth[2]
152 except IOError:
153 pass
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000154 # Perform NNRP authentication if needed.
Tim Peters2344fae2001-01-15 00:50:52 +0000155 if user:
156 resp = self.shortcmd('authinfo user '+user)
157 if resp[:3] == '381':
158 if not password:
159 raise NNTPReplyError(resp)
160 else:
161 resp = self.shortcmd(
162 'authinfo pass '+password)
163 if resp[:3] != '281':
164 raise NNTPPermanentError(resp)
Thomas Wouters47adcba2001-01-16 06:35:14 +0000165 if readermode_afterauth:
166 try:
167 self.welcome = self.shortcmd('mode reader')
168 except NNTPPermanentError:
169 # error 500, probably 'not implemented'
170 pass
Tim Petersdfb673b2001-01-16 07:12:46 +0000171
Barry Warsaw9dd78722000-02-10 20:25:53 +0000172
Tim Peters2344fae2001-01-15 00:50:52 +0000173 # 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 Rossumc629d341992-11-05 10:43:02 +0000177
Tim Peters2344fae2001-01-15 00:50:52 +0000178 def getwelcome(self):
179 """Get the welcome message from the server
180 (this is read and squirreled away by __init__()).
181 If the response code is 200, posting is allowed;
182 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000183
Walter Dörwald70a6b492004-02-12 17:35:32 +0000184 if self.debugging: print '*welcome*', repr(self.welcome)
Tim Peters2344fae2001-01-15 00:50:52 +0000185 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000186
Tim Peters2344fae2001-01-15 00:50:52 +0000187 def set_debuglevel(self, level):
188 """Set the debugging level. Argument 'level' means:
189 0: no debugging output (default)
190 1: print commands and responses but not body text etc.
191 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000192
Tim Peters2344fae2001-01-15 00:50:52 +0000193 self.debugging = level
194 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000195
Tim Peters2344fae2001-01-15 00:50:52 +0000196 def putline(self, line):
197 """Internal: send one line to the server, appending CRLF."""
198 line = line + CRLF
Walter Dörwald70a6b492004-02-12 17:35:32 +0000199 if self.debugging > 1: print '*put*', repr(line)
Martin v. Löwise12454f2002-02-16 23:06:19 +0000200 self.sock.sendall(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000201
Tim Peters2344fae2001-01-15 00:50:52 +0000202 def putcmd(self, line):
203 """Internal: send one command to the server (through putline())."""
Walter Dörwald70a6b492004-02-12 17:35:32 +0000204 if self.debugging: print '*cmd*', repr(line)
Tim Peters2344fae2001-01-15 00:50:52 +0000205 self.putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000206
Tim Peters2344fae2001-01-15 00:50:52 +0000207 def getline(self):
208 """Internal: return one line from the server, stripping CRLF.
209 Raise EOFError if the connection is closed."""
Barry Warsaw42faa552013-09-30 18:35:15 -0400210 line = self.file.readline(_MAXLINE + 1)
211 if len(line) > _MAXLINE:
212 raise NNTPDataError('line too long')
Tim Peters2344fae2001-01-15 00:50:52 +0000213 if self.debugging > 1:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000214 print '*get*', repr(line)
Tim Peters2344fae2001-01-15 00:50:52 +0000215 if not line: raise EOFError
216 if line[-2:] == CRLF: line = line[:-2]
217 elif line[-1:] in CRLF: line = line[:-1]
218 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000219
Tim Peters2344fae2001-01-15 00:50:52 +0000220 def getresp(self):
221 """Internal: get a response from the server.
222 Raise various errors if the response indicates an error."""
223 resp = self.getline()
Walter Dörwald70a6b492004-02-12 17:35:32 +0000224 if self.debugging: print '*resp*', repr(resp)
Tim Peters2344fae2001-01-15 00:50:52 +0000225 c = resp[:1]
226 if c == '4':
227 raise NNTPTemporaryError(resp)
228 if c == '5':
229 raise NNTPPermanentError(resp)
230 if c not in '123':
231 raise NNTPProtocolError(resp)
232 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000233
Fredrik Lundha5e61652001-10-18 20:58:25 +0000234 def getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000235 """Internal: get a response plus following text from the server.
236 Raise various errors if the response indicates an error."""
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000237
238 openedFile = None
239 try:
240 # If a string was passed then open a file with that name
Walter Dörwald65230a22002-06-03 15:58:32 +0000241 if isinstance(file, str):
Fredrik Lundha5e61652001-10-18 20:58:25 +0000242 openedFile = file = open(file, "w")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000243
244 resp = self.getresp()
245 if resp[:3] not in LONGRESP:
246 raise NNTPReplyError(resp)
247 list = []
248 while 1:
249 line = self.getline()
250 if line == '.':
251 break
252 if line[:2] == '..':
253 line = line[1:]
Fredrik Lundha5e61652001-10-18 20:58:25 +0000254 if file:
255 file.write(line + "\n")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000256 else:
257 list.append(line)
258 finally:
259 # If this method created the file, then it must close it
260 if openedFile:
261 openedFile.close()
262
Tim Peters2344fae2001-01-15 00:50:52 +0000263 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000264
Tim Peters2344fae2001-01-15 00:50:52 +0000265 def shortcmd(self, line):
266 """Internal: send a command and get the response."""
267 self.putcmd(line)
268 return self.getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000269
Fredrik Lundha5e61652001-10-18 20:58:25 +0000270 def longcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000271 """Internal: send a command and get the response plus following text."""
272 self.putcmd(line)
Fredrik Lundha5e61652001-10-18 20:58:25 +0000273 return self.getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000274
Guido van Rossuma2685402003-04-19 18:04:57 +0000275 def newgroups(self, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000276 """Process a NEWGROUPS command. Arguments:
277 - date: string 'yymmdd' indicating the date
278 - time: string 'hhmmss' indicating the time
279 Return:
280 - resp: server response if successful
281 - list: list of newsgroup names"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000282
Guido van Rossuma2685402003-04-19 18:04:57 +0000283 return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000284
Guido van Rossuma2685402003-04-19 18:04:57 +0000285 def newnews(self, group, date, time, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000286 """Process a NEWNEWS command. Arguments:
287 - group: group name or '*'
288 - date: string 'yymmdd' indicating the date
289 - time: string 'hhmmss' indicating the time
290 Return:
291 - resp: server response if successful
Georg Brandl5dbda752005-07-17 20:27:41 +0000292 - list: list of message ids"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000293
Tim Peters2344fae2001-01-15 00:50:52 +0000294 cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
Guido van Rossuma2685402003-04-19 18:04:57 +0000295 return self.longcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000296
Guido van Rossuma2685402003-04-19 18:04:57 +0000297 def list(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000298 """Process a LIST command. Return:
299 - resp: server response if successful
300 - list: list of (group, last, first, flag) (strings)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000301
Guido van Rossuma2685402003-04-19 18:04:57 +0000302 resp, list = self.longcmd('LIST', file)
Tim Peters2344fae2001-01-15 00:50:52 +0000303 for i in range(len(list)):
304 # Parse lines into "group last first flag"
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000305 list[i] = tuple(list[i].split())
Tim Peters2344fae2001-01-15 00:50:52 +0000306 return resp, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000307
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000308 def description(self, group):
309
310 """Get a description for a single group. If more than one
311 group matches ('group' is a pattern), return the first. If no
312 group matches, return an empty string.
313
314 This elides the response code from the server, since it can
315 only be '215' or '285' (for xgtitle) anyway. If the response
316 code is needed, use the 'descriptions' method.
317
318 NOTE: This neither checks for a wildcard in 'group' nor does
319 it check whether the group actually exists."""
320
321 resp, lines = self.descriptions(group)
322 if len(lines) == 0:
323 return ""
324 else:
325 return lines[0][1]
326
327 def descriptions(self, group_pattern):
328 """Get descriptions for a range of groups."""
329 line_pat = re.compile("^(?P<group>[^ \t]+)[ \t]+(.*)$")
330 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
331 resp, raw_lines = self.longcmd('LIST NEWSGROUPS ' + group_pattern)
332 if resp[:3] != "215":
333 # Now the deprecated XGTITLE. This either raises an error
334 # or succeeds with the same output structure as LIST
335 # NEWSGROUPS.
336 resp, raw_lines = self.longcmd('XGTITLE ' + group_pattern)
337 lines = []
338 for raw_line in raw_lines:
339 match = line_pat.search(raw_line.strip())
340 if match:
341 lines.append(match.group(1, 2))
342 return resp, lines
343
Tim Peters2344fae2001-01-15 00:50:52 +0000344 def group(self, name):
345 """Process a GROUP command. Argument:
346 - group: the group name
347 Returns:
348 - resp: server response if successful
349 - count: number of articles (string)
350 - first: first article number (string)
351 - last: last article number (string)
352 - name: the group name"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000353
Tim Peters2344fae2001-01-15 00:50:52 +0000354 resp = self.shortcmd('GROUP ' + name)
355 if resp[:3] != '211':
356 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000357 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000358 count = first = last = 0
359 n = len(words)
360 if n > 1:
361 count = words[1]
362 if n > 2:
363 first = words[2]
364 if n > 3:
365 last = words[3]
366 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000367 name = words[4].lower()
Tim Peters2344fae2001-01-15 00:50:52 +0000368 return resp, count, first, last, name
Guido van Rossumc629d341992-11-05 10:43:02 +0000369
Guido van Rossuma2685402003-04-19 18:04:57 +0000370 def help(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000371 """Process a HELP command. Returns:
372 - resp: server response if successful
373 - list: list of strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000374
Guido van Rossuma2685402003-04-19 18:04:57 +0000375 return self.longcmd('HELP',file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000376
Tim Peters2344fae2001-01-15 00:50:52 +0000377 def statparse(self, resp):
378 """Internal: parse the response of a STAT, NEXT or LAST command."""
379 if resp[:2] != '22':
380 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000381 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000382 nr = 0
383 id = ''
384 n = len(words)
385 if n > 1:
386 nr = words[1]
387 if n > 2:
388 id = words[2]
389 return resp, nr, id
Guido van Rossumc629d341992-11-05 10:43:02 +0000390
Tim Peters2344fae2001-01-15 00:50:52 +0000391 def statcmd(self, line):
392 """Internal: process a STAT, NEXT or LAST command."""
393 resp = self.shortcmd(line)
394 return self.statparse(resp)
Guido van Rossumc629d341992-11-05 10:43:02 +0000395
Tim Peters2344fae2001-01-15 00:50:52 +0000396 def stat(self, id):
397 """Process a STAT command. Argument:
398 - id: article number or message id
399 Returns:
400 - resp: server response if successful
401 - nr: the article number
Georg Brandl5dbda752005-07-17 20:27:41 +0000402 - id: the message id"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000403
Tim Peters2344fae2001-01-15 00:50:52 +0000404 return self.statcmd('STAT ' + id)
Guido van Rossumc629d341992-11-05 10:43:02 +0000405
Tim Peters2344fae2001-01-15 00:50:52 +0000406 def next(self):
407 """Process a NEXT command. No arguments. Return as for STAT."""
408 return self.statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000409
Tim Peters2344fae2001-01-15 00:50:52 +0000410 def last(self):
411 """Process a LAST command. No arguments. Return as for STAT."""
412 return self.statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000413
Fredrik Lundha5e61652001-10-18 20:58:25 +0000414 def artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000415 """Internal: process a HEAD, BODY or ARTICLE command."""
Fredrik Lundha5e61652001-10-18 20:58:25 +0000416 resp, list = self.longcmd(line, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000417 resp, nr, id = self.statparse(resp)
418 return resp, nr, id, list
Guido van Rossumc629d341992-11-05 10:43:02 +0000419
Tim Peters2344fae2001-01-15 00:50:52 +0000420 def head(self, id):
421 """Process a HEAD command. Argument:
422 - id: article number or message id
423 Returns:
424 - resp: server response if successful
425 - nr: article number
426 - id: message id
427 - list: the lines of the article's header"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000428
Tim Peters2344fae2001-01-15 00:50:52 +0000429 return self.artcmd('HEAD ' + id)
Guido van Rossumc629d341992-11-05 10:43:02 +0000430
Fredrik Lundha5e61652001-10-18 20:58:25 +0000431 def body(self, id, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000432 """Process a BODY command. Argument:
433 - id: article number or message id
Fredrik Lundha5e61652001-10-18 20:58:25 +0000434 - file: Filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000435 Returns:
436 - resp: server response if successful
437 - nr: article number
438 - id: message id
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000439 - list: the lines of the article's body or an empty list
Fredrik Lundha5e61652001-10-18 20:58:25 +0000440 if file was used"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000441
Fredrik Lundha5e61652001-10-18 20:58:25 +0000442 return self.artcmd('BODY ' + id, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000443
Tim Peters2344fae2001-01-15 00:50:52 +0000444 def article(self, id):
445 """Process an ARTICLE command. Argument:
446 - id: article number or message id
447 Returns:
448 - resp: server response if successful
449 - nr: article number
450 - id: message id
451 - list: the lines of the article"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000452
Tim Peters2344fae2001-01-15 00:50:52 +0000453 return self.artcmd('ARTICLE ' + id)
Guido van Rossumc629d341992-11-05 10:43:02 +0000454
Tim Peters2344fae2001-01-15 00:50:52 +0000455 def slave(self):
456 """Process a SLAVE command. Returns:
457 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000458
Tim Peters2344fae2001-01-15 00:50:52 +0000459 return self.shortcmd('SLAVE')
Guido van Rossumc629d341992-11-05 10:43:02 +0000460
Guido van Rossuma2685402003-04-19 18:04:57 +0000461 def xhdr(self, hdr, str, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000462 """Process an XHDR command (optional server extension). Arguments:
463 - hdr: the header type (e.g. 'subject')
464 - str: an article nr, a message id, or a range nr1-nr2
465 Returns:
466 - resp: server response if successful
467 - list: list of (nr, value) strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000468
Tim Peters2344fae2001-01-15 00:50:52 +0000469 pat = re.compile('^([0-9]+) ?(.*)\n?')
Guido van Rossuma2685402003-04-19 18:04:57 +0000470 resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000471 for i in range(len(lines)):
472 line = lines[i]
473 m = pat.match(line)
474 if m:
475 lines[i] = m.group(1, 2)
476 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000477
Guido van Rossuma2685402003-04-19 18:04:57 +0000478 def xover(self, start, end, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000479 """Process an XOVER command (optional server extension) Arguments:
480 - start: start of range
481 - end: end of range
482 Returns:
483 - resp: server response if successful
484 - list: list of (art-nr, subject, poster, date,
485 id, references, size, lines)"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000486
Guido van Rossuma2685402003-04-19 18:04:57 +0000487 resp, lines = self.longcmd('XOVER ' + start + '-' + end, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000488 xover_lines = []
489 for line in lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000490 elem = line.split("\t")
Tim Peters2344fae2001-01-15 00:50:52 +0000491 try:
492 xover_lines.append((elem[0],
493 elem[1],
494 elem[2],
495 elem[3],
496 elem[4],
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000497 elem[5].split(),
Tim Peters2344fae2001-01-15 00:50:52 +0000498 elem[6],
499 elem[7]))
500 except IndexError:
501 raise NNTPDataError(line)
502 return resp,xover_lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000503
Guido van Rossuma2685402003-04-19 18:04:57 +0000504 def xgtitle(self, group, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000505 """Process an XGTITLE command (optional server extension) Arguments:
506 - group: group name wildcard (i.e. news.*)
507 Returns:
508 - resp: server response if successful
509 - list: list of (name,title) strings"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000510
Tim Peters2344fae2001-01-15 00:50:52 +0000511 line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
Guido van Rossuma2685402003-04-19 18:04:57 +0000512 resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000513 lines = []
514 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000515 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000516 if match:
517 lines.append(match.group(1, 2))
518 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000519
Tim Peters2344fae2001-01-15 00:50:52 +0000520 def xpath(self,id):
521 """Process an XPATH command (optional server extension) Arguments:
522 - id: Message id of article
523 Returns:
524 resp: server response if successful
525 path: directory path to article"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000526
Tim Peters2344fae2001-01-15 00:50:52 +0000527 resp = self.shortcmd("XPATH " + id)
528 if resp[:3] != '223':
529 raise NNTPReplyError(resp)
530 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000531 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000532 except ValueError:
533 raise NNTPReplyError(resp)
534 else:
535 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000536
Tim Peters2344fae2001-01-15 00:50:52 +0000537 def date (self):
538 """Process the DATE command. Arguments:
539 None
540 Returns:
541 resp: server response if successful
542 date: Date suitable for newnews/newgroups commands etc.
543 time: Time suitable for newnews/newgroups commands etc."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000544
Tim Peters2344fae2001-01-15 00:50:52 +0000545 resp = self.shortcmd("DATE")
546 if resp[:3] != '111':
547 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000548 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000549 if len(elem) != 2:
550 raise NNTPDataError(resp)
551 date = elem[1][2:8]
552 time = elem[1][-6:]
553 if len(date) != 6 or len(time) != 6:
554 raise NNTPDataError(resp)
555 return resp, date, time
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000556
557
Tim Peters2344fae2001-01-15 00:50:52 +0000558 def post(self, f):
559 """Process a POST command. Arguments:
560 - f: file containing the article
561 Returns:
562 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000563
Tim Peters2344fae2001-01-15 00:50:52 +0000564 resp = self.shortcmd('POST')
565 # Raises error_??? if posting is not allowed
566 if resp[0] != '3':
567 raise NNTPReplyError(resp)
568 while 1:
569 line = f.readline()
570 if not line:
571 break
572 if line[-1] == '\n':
573 line = line[:-1]
574 if line[:1] == '.':
575 line = '.' + line
576 self.putline(line)
577 self.putline('.')
578 return self.getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000579
Tim Peters2344fae2001-01-15 00:50:52 +0000580 def ihave(self, id, f):
581 """Process an IHAVE command. Arguments:
582 - id: message-id of the article
583 - f: file containing the article
584 Returns:
585 - resp: server response if successful
586 Note that if the server refuses the article an exception is raised."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000587
Tim Peters2344fae2001-01-15 00:50:52 +0000588 resp = self.shortcmd('IHAVE ' + id)
589 # Raises error_??? if the server already has it
590 if resp[0] != '3':
591 raise NNTPReplyError(resp)
592 while 1:
593 line = f.readline()
594 if not line:
595 break
596 if line[-1] == '\n':
597 line = line[:-1]
598 if line[:1] == '.':
599 line = '.' + line
600 self.putline(line)
601 self.putline('.')
602 return self.getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000603
Tim Peters2344fae2001-01-15 00:50:52 +0000604 def quit(self):
605 """Process a QUIT command and close the socket. Returns:
606 - resp: server response if successful"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000607
Tim Peters2344fae2001-01-15 00:50:52 +0000608 resp = self.shortcmd('QUIT')
609 self.file.close()
610 self.sock.close()
611 del self.file, self.sock
612 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000613
614
Neal Norwitzef679562002-11-14 02:19:44 +0000615# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000616# Assumption: if there's a local news server, it's called 'news'.
617# Assumption: if user queries a remote news server, it's named
618# in the environment variable NNTPSERVER (used by slrn and kin)
619# and we want readermode off.
620if __name__ == '__main__':
621 import os
622 newshost = 'news' and os.environ["NNTPSERVER"]
623 if newshost.find('.') == -1:
624 mode = 'readermode'
625 else:
626 mode = None
627 s = NNTP(newshost, readermode=mode)
Tim Peters2344fae2001-01-15 00:50:52 +0000628 resp, count, first, last, name = s.group('comp.lang.python')
629 print resp
630 print 'Group', name, 'has', count, 'articles, range', first, 'to', last
631 resp, subs = s.xhdr('subject', first + '-' + last)
632 print resp
633 for item in subs:
634 print "%7s %s" % item
635 resp = s.quit()
636 print resp