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 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,
95 readermode
=None, usenetrc
=True):
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
.create_connection((host
, port
))
113 self
.file = self
.sock
.makefile('rb')
115 self
.welcome
= self
.getresp()
117 # 'mode reader' is sometimes necessary to enable 'reader' mode.
118 # However, the order in which 'mode reader' and 'authinfo' need to
119 # 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
125 self
.welcome
= self
.shortcmd('mode reader')
126 except NNTPPermanentError
:
127 # error 500, probably 'not implemented'
129 except NNTPTemporaryError
, e
:
130 if user
and e
.response
[:3] == '480':
131 # Need authorization before 'mode reader'
132 readermode_afterauth
= 1
135 # If no login/password was specified, try to get them from ~/.netrc
136 # Presume that if .netc has an entry, NNRP authentication is required.
138 if usenetrc
and not user
:
140 credentials
= netrc
.netrc()
141 auth
= credentials
.authenticators(host
)
147 # Perform NNRP authentication if needed.
149 resp
= self
.shortcmd('authinfo user '+user
)
150 if resp
[:3] == '381':
152 raise NNTPReplyError(resp
)
154 resp
= self
.shortcmd(
155 'authinfo pass '+password
)
156 if resp
[:3] != '281':
157 raise NNTPPermanentError(resp
)
158 if readermode_afterauth
:
160 self
.welcome
= self
.shortcmd('mode reader')
161 except NNTPPermanentError
:
162 # error 500, probably 'not implemented'
166 # 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
171 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."""
177 if self
.debugging
: print '*welcome*', repr(self
.welcome
)
180 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"""
186 self
.debugging
= level
187 debug
= set_debuglevel
189 def putline(self
, line
):
190 """Internal: send one line to the server, appending CRLF."""
192 if self
.debugging
> 1: print '*put*', repr(line
)
193 self
.sock
.sendall(line
)
195 def putcmd(self
, line
):
196 """Internal: send one command to the server (through putline())."""
197 if self
.debugging
: print '*cmd*', repr(line
)
201 """Internal: return one line from the server, stripping CRLF.
202 Raise EOFError if the connection is closed."""
203 line
= self
.file.readline()
204 if self
.debugging
> 1:
205 print '*get*', repr(line
)
206 if not line
: raise EOFError
207 if line
[-2:] == CRLF
: line
= line
[:-2]
208 elif line
[-1:] in CRLF
: line
= line
[:-1]
212 """Internal: get a response from the server.
213 Raise various errors if the response indicates an error."""
214 resp
= self
.getline()
215 if self
.debugging
: print '*resp*', repr(resp
)
218 raise NNTPTemporaryError(resp
)
220 raise NNTPPermanentError(resp
)
222 raise NNTPProtocolError(resp
)
225 def getlongresp(self
, file=None):
226 """Internal: get a response plus following text from the server.
227 Raise various errors if the response indicates an error."""
231 # If a string was passed then open a file with that name
232 if isinstance(file, str):
233 openedFile
= file = open(file, "w")
235 resp
= self
.getresp()
236 if resp
[:3] not in LONGRESP
:
237 raise NNTPReplyError(resp
)
240 line
= self
.getline()
246 file.write(line
+ "\n")
250 # If this method created the file, then it must close it
256 def shortcmd(self
, line
):
257 """Internal: send a command and get the response."""
259 return self
.getresp()
261 def longcmd(self
, line
, file=None):
262 """Internal: send a command and get the response plus following text."""
264 return self
.getlongresp(file)
266 def newgroups(self
, date
, time
, file=None):
267 """Process a NEWGROUPS command. Arguments:
268 - date: string 'yymmdd' indicating the date
269 - time: string 'hhmmss' indicating the time
271 - resp: server response if successful
272 - list: list of newsgroup names"""
274 return self
.longcmd('NEWGROUPS ' + date
+ ' ' + time
, file)
276 def newnews(self
, group
, date
, time
, file=None):
277 """Process a NEWNEWS command. Arguments:
278 - group: group name or '*'
279 - date: string 'yymmdd' indicating the date
280 - time: string 'hhmmss' indicating the time
282 - resp: server response if successful
283 - list: list of message ids"""
285 cmd
= 'NEWNEWS ' + group
+ ' ' + date
+ ' ' + time
286 return self
.longcmd(cmd
, file)
288 def list(self
, file=None):
289 """Process a LIST command. Return:
290 - resp: server response if successful
291 - list: list of (group, last, first, flag) (strings)"""
293 resp
, list = self
.longcmd('LIST', file)
294 for i
in range(len(list)):
295 # Parse lines into "group last first flag"
296 list[i
] = tuple(list[i
].split())
299 def description(self
, group
):
301 """Get a description for a single group. If more than one
302 group matches ('group' is a pattern), return the first. If no
303 group matches, return an empty string.
305 This elides the response code from the server, since it can
306 only be '215' or '285' (for xgtitle) anyway. If the response
307 code is needed, use the 'descriptions' method.
309 NOTE: This neither checks for a wildcard in 'group' nor does
310 it check whether the group actually exists."""
312 resp
, lines
= self
.descriptions(group
)
318 def descriptions(self
, group_pattern
):
319 """Get descriptions for a range of groups."""
320 line_pat
= re
.compile("^(?P<group>[^ \t]+)[ \t]+(.*)$")
321 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
322 resp
, raw_lines
= self
.longcmd('LIST NEWSGROUPS ' + group_pattern
)
323 if resp
[:3] != "215":
324 # Now the deprecated XGTITLE. This either raises an error
325 # or succeeds with the same output structure as LIST
327 resp
, raw_lines
= self
.longcmd('XGTITLE ' + group_pattern
)
329 for raw_line
in raw_lines
:
330 match
= line_pat
.search(raw_line
.strip())
332 lines
.append(match
.group(1, 2))
335 def group(self
, name
):
336 """Process a GROUP command. Argument:
337 - group: the group name
339 - resp: server response if successful
340 - count: number of articles (string)
341 - first: first article number (string)
342 - last: last article number (string)
343 - name: the group name"""
345 resp
= self
.shortcmd('GROUP ' + name
)
346 if resp
[:3] != '211':
347 raise NNTPReplyError(resp
)
349 count
= first
= last
= 0
358 name
= words
[4].lower()
359 return resp
, count
, first
, last
, name
361 def help(self
, file=None):
362 """Process a HELP command. Returns:
363 - resp: server response if successful
364 - list: list of strings"""
366 return self
.longcmd('HELP',file)
368 def statparse(self
, resp
):
369 """Internal: parse the response of a STAT, NEXT or LAST command."""
371 raise NNTPReplyError(resp
)
382 def statcmd(self
, line
):
383 """Internal: process a STAT, NEXT or LAST command."""
384 resp
= self
.shortcmd(line
)
385 return self
.statparse(resp
)
388 """Process a STAT command. Argument:
389 - id: article number or message id
391 - resp: server response if successful
392 - nr: the article number
393 - id: the message id"""
395 return self
.statcmd('STAT ' + id)
398 """Process a NEXT command. No arguments. Return as for STAT."""
399 return self
.statcmd('NEXT')
402 """Process a LAST command. No arguments. Return as for STAT."""
403 return self
.statcmd('LAST')
405 def artcmd(self
, line
, file=None):
406 """Internal: process a HEAD, BODY or ARTICLE command."""
407 resp
, list = self
.longcmd(line
, file)
408 resp
, nr
, id = self
.statparse(resp
)
409 return resp
, nr
, id, list
412 """Process a HEAD command. Argument:
413 - id: article number or message id
415 - resp: server response if successful
418 - list: the lines of the article's header"""
420 return self
.artcmd('HEAD ' + id)
422 def body(self
, id, file=None):
423 """Process a BODY command. Argument:
424 - id: article number or message id
425 - file: Filename string or file object to store the article in
427 - resp: server response if successful
430 - list: the lines of the article's body or an empty list
433 return self
.artcmd('BODY ' + id, file)
435 def article(self
, id):
436 """Process an ARTICLE command. Argument:
437 - id: article number or message id
439 - resp: server response if successful
442 - list: the lines of the article"""
444 return self
.artcmd('ARTICLE ' + id)
447 """Process a SLAVE command. Returns:
448 - resp: server response if successful"""
450 return self
.shortcmd('SLAVE')
452 def xhdr(self
, hdr
, str, file=None):
453 """Process an XHDR command (optional server extension). Arguments:
454 - hdr: the header type (e.g. 'subject')
455 - str: an article nr, a message id, or a range nr1-nr2
457 - resp: server response if successful
458 - list: list of (nr, value) strings"""
460 pat
= re
.compile('^([0-9]+) ?(.*)\n?')
461 resp
, lines
= self
.longcmd('XHDR ' + hdr
+ ' ' + str, file)
462 for i
in range(len(lines
)):
466 lines
[i
] = m
.group(1, 2)
469 def xover(self
, start
, end
, file=None):
470 """Process an XOVER command (optional server extension) Arguments:
471 - start: start of range
474 - resp: server response if successful
475 - list: list of (art-nr, subject, poster, date,
476 id, references, size, lines)"""
478 resp
, lines
= self
.longcmd('XOVER ' + start
+ '-' + end
, file)
481 elem
= line
.split("\t")
483 xover_lines
.append((elem
[0],
492 raise NNTPDataError(line
)
493 return resp
,xover_lines
495 def xgtitle(self
, group
, file=None):
496 """Process an XGTITLE command (optional server extension) Arguments:
497 - group: group name wildcard (i.e. news.*)
499 - resp: server response if successful
500 - list: list of (name,title) strings"""
502 line_pat
= re
.compile("^([^ \t]+)[ \t]+(.*)$")
503 resp
, raw_lines
= self
.longcmd('XGTITLE ' + group
, file)
505 for raw_line
in raw_lines
:
506 match
= line_pat
.search(raw_line
.strip())
508 lines
.append(match
.group(1, 2))
512 """Process an XPATH command (optional server extension) Arguments:
513 - id: Message id of article
515 resp: server response if successful
516 path: directory path to article"""
518 resp
= self
.shortcmd("XPATH " + id)
519 if resp
[:3] != '223':
520 raise NNTPReplyError(resp
)
522 [resp_num
, path
] = resp
.split()
524 raise NNTPReplyError(resp
)
529 """Process the DATE command. Arguments:
532 resp: server response if successful
533 date: Date suitable for newnews/newgroups commands etc.
534 time: Time suitable for newnews/newgroups commands etc."""
536 resp
= self
.shortcmd("DATE")
537 if resp
[:3] != '111':
538 raise NNTPReplyError(resp
)
541 raise NNTPDataError(resp
)
544 if len(date
) != 6 or len(time
) != 6:
545 raise NNTPDataError(resp
)
546 return resp
, date
, time
550 """Process a POST command. Arguments:
551 - f: file containing the article
553 - resp: server response if successful"""
555 resp
= self
.shortcmd('POST')
556 # Raises error_??? if posting is not allowed
558 raise NNTPReplyError(resp
)
569 return self
.getresp()
571 def ihave(self
, id, f
):
572 """Process an IHAVE command. Arguments:
573 - id: message-id of the article
574 - f: file containing the article
576 - resp: server response if successful
577 Note that if the server refuses the article an exception is raised."""
579 resp
= self
.shortcmd('IHAVE ' + id)
580 # Raises error_??? if the server already has it
582 raise NNTPReplyError(resp
)
593 return self
.getresp()
596 """Process a QUIT command and close the socket. Returns:
597 - resp: server response if successful"""
599 resp
= self
.shortcmd('QUIT')
602 del self
.file, self
.sock
606 # Test retrieval when run as a script.
607 # Assumption: if there's a local news server, it's called 'news'.
608 # Assumption: if user queries a remote news server, it's named
609 # in the environment variable NNTPSERVER (used by slrn and kin)
610 # and we want readermode off.
611 if __name__
== '__main__':
613 newshost
= 'news' and os
.environ
["NNTPSERVER"]
614 if newshost
.find('.') == -1:
618 s
= NNTP(newshost
, readermode
=mode
)
619 resp
, count
, first
, last
, name
= s
.group('comp.lang.python')
621 print 'Group', name
, 'has', count
, 'articles, range', first
, 'to', last
622 resp
, subs
= s
.xhdr('subject', first
+ '-' + last
)
625 print "%7s %s" % item