1 """An NNTP client class based on RFC 977: Network News Transfer Protocol.
5 >>> from nntplib import NNTP
7 >>> resp, count, first, last, name = s.group('comp.lang.python')
8 >>> print 'Group', name, 'has', count, 'articles, range', first, 'to', last
9 Group comp.lang.python has 51 articles, range 5770 to 5821
10 >>> resp, subs = s.xhdr('subject', first + '-' + last)
14 Here 'resp' is the server response line.
15 Error responses are turned into exceptions.
17 To post an article from a file:
18 >>> f = open(filename, 'r') # file containing article, including header
22 For descriptions of all methods, read the comments in the code below.
23 Note that all arguments and return values representing article numbers
24 are strings, not numbers, since they are rarely used for calculations.
27 # RFC 977 by Brian Kantor and Phil Lapsley.
28 # xover, xgtitle, xpath, date methods by Kevan Heydon
35 __all__
= ["NNTP","NNTPReplyError","NNTPTemporaryError",
36 "NNTPPermanentError","NNTPProtocolError","NNTPDataError",
37 "error_reply","error_temp","error_perm","error_proto",
40 # Exceptions raised when an error or invalid response is received
41 class NNTPError(Exception):
42 """Base class for all nntplib exceptions"""
43 def __init__(self
, *args
):
44 apply(Exception.__init
__, (self
,)+args
)
46 self
.response
= args
[0]
48 self
.response
= 'No response given'
50 class NNTPReplyError(NNTPError
):
51 """Unexpected [123]xx reply"""
54 class NNTPTemporaryError(NNTPError
):
58 class NNTPPermanentError(NNTPError
):
62 class NNTPProtocolError(NNTPError
):
63 """Response does not begin with [1-5]"""
66 class NNTPDataError(NNTPError
):
67 """Error in response data"""
70 # for backwards compatibility
71 error_reply
= NNTPReplyError
72 error_temp
= NNTPTemporaryError
73 error_perm
= NNTPPermanentError
74 error_proto
= NNTPProtocolError
75 error_data
= NNTPDataError
79 # Standard port used by NNTP servers
83 # Response numbers that are followed by additional text (e.g. article)
84 LONGRESP
= ['100', '215', '220', '221', '222', '224', '230', '231', '282']
87 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
94 def __init__(self
, host
, port
=NNTP_PORT
, user
=None, password
=None,
96 """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
104 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
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')
116 self
.welcome
= self
.getresp()
118 # 'mode reader' is sometimes necessary to enable 'reader' mode.
119 # However, the order in which 'mode reader' and 'authinfo' need to
120 # 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
126 self
.welcome
= self
.shortcmd('mode reader')
127 except NNTPPermanentError
:
128 # error 500, probably 'not implemented'
130 except NNTPTemporaryError
, e
:
131 if user
and e
.response
[:3] == '480':
132 # Need authorization before 'mode reader'
133 readermode_afterauth
= 1
137 resp
= self
.shortcmd('authinfo user '+user
)
138 if resp
[:3] == '381':
140 raise NNTPReplyError(resp
)
142 resp
= self
.shortcmd(
143 'authinfo pass '+password
)
144 if resp
[:3] != '281':
145 raise NNTPPermanentError(resp
)
146 if readermode_afterauth
:
148 self
.welcome
= self
.shortcmd('mode reader')
149 except NNTPPermanentError
:
150 # error 500, probably 'not implemented'
154 # Get the welcome message from the server
155 # (this is read and squirreled away by __init__()).
156 # If the response code is 200, posting is allowed;
157 # if it 201, posting is not allowed
159 def getwelcome(self
):
160 """Get the welcome message from the server
161 (this is read and squirreled away by __init__()).
162 If the response code is 200, posting is allowed;
163 if it 201, posting is not allowed."""
165 if self
.debugging
: print '*welcome*', `self
.welcome`
168 def set_debuglevel(self
, level
):
169 """Set the debugging level. Argument 'level' means:
170 0: no debugging output (default)
171 1: print commands and responses but not body text etc.
172 2: also print raw lines read and sent before stripping CR/LF"""
174 self
.debugging
= level
175 debug
= set_debuglevel
177 def putline(self
, line
):
178 """Internal: send one line to the server, appending CRLF."""
180 if self
.debugging
> 1: print '*put*', `line`
183 def putcmd(self
, line
):
184 """Internal: send one command to the server (through putline())."""
185 if self
.debugging
: print '*cmd*', `line`
189 """Internal: return one line from the server, stripping CRLF.
190 Raise EOFError if the connection is closed."""
191 line
= self
.file.readline()
192 if self
.debugging
> 1:
193 print '*get*', `line`
194 if not line
: raise EOFError
195 if line
[-2:] == CRLF
: line
= line
[:-2]
196 elif line
[-1:] in CRLF
: line
= line
[:-1]
200 """Internal: get a response from the server.
201 Raise various errors if the response indicates an error."""
202 resp
= self
.getline()
203 if self
.debugging
: print '*resp*', `resp`
206 raise NNTPTemporaryError(resp
)
208 raise NNTPPermanentError(resp
)
210 raise NNTPProtocolError(resp
)
213 def getlongresp(self
):
214 """Internal: get a response plus following text from the server.
215 Raise various errors if the response indicates an error."""
216 resp
= self
.getresp()
217 if resp
[:3] not in LONGRESP
:
218 raise NNTPReplyError(resp
)
221 line
= self
.getline()
229 def shortcmd(self
, line
):
230 """Internal: send a command and get the response."""
232 return self
.getresp()
234 def longcmd(self
, line
):
235 """Internal: send a command and get the response plus following text."""
237 return self
.getlongresp()
239 def newgroups(self
, date
, time
):
240 """Process a NEWGROUPS command. Arguments:
241 - date: string 'yymmdd' indicating the date
242 - time: string 'hhmmss' indicating the time
244 - resp: server response if successful
245 - list: list of newsgroup names"""
247 return self
.longcmd('NEWGROUPS ' + date
+ ' ' + time
)
249 def newnews(self
, group
, date
, time
):
250 """Process a NEWNEWS command. Arguments:
251 - group: group name or '*'
252 - date: string 'yymmdd' indicating the date
253 - time: string 'hhmmss' indicating the time
255 - resp: server response if successful
256 - list: list of article ids"""
258 cmd
= 'NEWNEWS ' + group
+ ' ' + date
+ ' ' + time
259 return self
.longcmd(cmd
)
262 """Process a LIST command. Return:
263 - resp: server response if successful
264 - list: list of (group, last, first, flag) (strings)"""
266 resp
, list = self
.longcmd('LIST')
267 for i
in range(len(list)):
268 # Parse lines into "group last first flag"
269 list[i
] = tuple(list[i
].split())
272 def group(self
, name
):
273 """Process a GROUP command. Argument:
274 - group: the group name
276 - resp: server response if successful
277 - count: number of articles (string)
278 - first: first article number (string)
279 - last: last article number (string)
280 - name: the group name"""
282 resp
= self
.shortcmd('GROUP ' + name
)
283 if resp
[:3] != '211':
284 raise NNTPReplyError(resp
)
286 count
= first
= last
= 0
295 name
= words
[4].lower()
296 return resp
, count
, first
, last
, name
299 """Process a HELP command. Returns:
300 - resp: server response if successful
301 - list: list of strings"""
303 return self
.longcmd('HELP')
305 def statparse(self
, resp
):
306 """Internal: parse the response of a STAT, NEXT or LAST command."""
308 raise NNTPReplyError(resp
)
319 def statcmd(self
, line
):
320 """Internal: process a STAT, NEXT or LAST command."""
321 resp
= self
.shortcmd(line
)
322 return self
.statparse(resp
)
325 """Process a STAT command. Argument:
326 - id: article number or message id
328 - resp: server response if successful
329 - nr: the article number
330 - id: the article id"""
332 return self
.statcmd('STAT ' + id)
335 """Process a NEXT command. No arguments. Return as for STAT."""
336 return self
.statcmd('NEXT')
339 """Process a LAST command. No arguments. Return as for STAT."""
340 return self
.statcmd('LAST')
342 def artcmd(self
, line
):
343 """Internal: process a HEAD, BODY or ARTICLE command."""
344 resp
, list = self
.longcmd(line
)
345 resp
, nr
, id = self
.statparse(resp
)
346 return resp
, nr
, id, list
349 """Process a HEAD command. Argument:
350 - id: article number or message id
352 - resp: server response if successful
355 - list: the lines of the article's header"""
357 return self
.artcmd('HEAD ' + id)
360 """Process a BODY command. Argument:
361 - id: article number or message id
363 - resp: server response if successful
366 - list: the lines of the article's body"""
368 return self
.artcmd('BODY ' + id)
370 def article(self
, id):
371 """Process an ARTICLE command. Argument:
372 - id: article number or message id
374 - resp: server response if successful
377 - list: the lines of the article"""
379 return self
.artcmd('ARTICLE ' + id)
382 """Process a SLAVE command. Returns:
383 - resp: server response if successful"""
385 return self
.shortcmd('SLAVE')
387 def xhdr(self
, hdr
, str):
388 """Process an XHDR command (optional server extension). Arguments:
389 - hdr: the header type (e.g. 'subject')
390 - str: an article nr, a message id, or a range nr1-nr2
392 - resp: server response if successful
393 - list: list of (nr, value) strings"""
395 pat
= re
.compile('^([0-9]+) ?(.*)\n?')
396 resp
, lines
= self
.longcmd('XHDR ' + hdr
+ ' ' + str)
397 for i
in range(len(lines
)):
401 lines
[i
] = m
.group(1, 2)
404 def xover(self
,start
,end
):
405 """Process an XOVER command (optional server extension) Arguments:
406 - start: start of range
409 - resp: server response if successful
410 - list: list of (art-nr, subject, poster, date,
411 id, references, size, lines)"""
413 resp
, lines
= self
.longcmd('XOVER ' + start
+ '-' + end
)
416 elem
= line
.split("\t")
418 xover_lines
.append((elem
[0],
427 raise NNTPDataError(line
)
428 return resp
,xover_lines
430 def xgtitle(self
, group
):
431 """Process an XGTITLE command (optional server extension) Arguments:
432 - group: group name wildcard (i.e. news.*)
434 - resp: server response if successful
435 - list: list of (name,title) strings"""
437 line_pat
= re
.compile("^([^ \t]+)[ \t]+(.*)$")
438 resp
, raw_lines
= self
.longcmd('XGTITLE ' + group
)
440 for raw_line
in raw_lines
:
441 match
= line_pat
.search(raw_line
.strip())
443 lines
.append(match
.group(1, 2))
447 """Process an XPATH command (optional server extension) Arguments:
448 - id: Message id of article
450 resp: server response if successful
451 path: directory path to article"""
453 resp
= self
.shortcmd("XPATH " + id)
454 if resp
[:3] != '223':
455 raise NNTPReplyError(resp
)
457 [resp_num
, path
] = resp
.split()
459 raise NNTPReplyError(resp
)
464 """Process the DATE command. Arguments:
467 resp: server response if successful
468 date: Date suitable for newnews/newgroups commands etc.
469 time: Time suitable for newnews/newgroups commands etc."""
471 resp
= self
.shortcmd("DATE")
472 if resp
[:3] != '111':
473 raise NNTPReplyError(resp
)
476 raise NNTPDataError(resp
)
479 if len(date
) != 6 or len(time
) != 6:
480 raise NNTPDataError(resp
)
481 return resp
, date
, time
485 """Process a POST command. Arguments:
486 - f: file containing the article
488 - resp: server response if successful"""
490 resp
= self
.shortcmd('POST')
491 # Raises error_??? if posting is not allowed
493 raise NNTPReplyError(resp
)
504 return self
.getresp()
506 def ihave(self
, id, f
):
507 """Process an IHAVE command. Arguments:
508 - id: message-id of the article
509 - f: file containing the article
511 - resp: server response if successful
512 Note that if the server refuses the article an exception is raised."""
514 resp
= self
.shortcmd('IHAVE ' + id)
515 # Raises error_??? if the server already has it
517 raise NNTPReplyError(resp
)
528 return self
.getresp()
531 """Process a QUIT command and close the socket. Returns:
532 - resp: server response if successful"""
534 resp
= self
.shortcmd('QUIT')
537 del self
.file, self
.sock
542 """Minimal test function."""
543 s
= NNTP('news', readermode
='reader')
544 resp
, count
, first
, last
, name
= s
.group('comp.lang.python')
546 print 'Group', name
, 'has', count
, 'articles, range', first
, 'to', last
547 resp
, subs
= s
.xhdr('subject', first
+ '-' + last
)
550 print "%7s %s" % item
555 # Run the test when run as a script
556 if __name__
== '__main__':