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
.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
136 # If no login/password was specified, try to get them from ~/.netrc
137 # Presume that if .netc has an entry, NNRP authentication is required.
139 if usenetrc
and not user
:
141 credentials
= netrc
.netrc()
142 auth
= credentials
.authenticators(host
)
148 # Perform NNRP authentication if needed.
150 resp
= self
.shortcmd('authinfo user '+user
)
151 if resp
[:3] == '381':
153 raise NNTPReplyError(resp
)
155 resp
= self
.shortcmd(
156 'authinfo pass '+password
)
157 if resp
[:3] != '281':
158 raise NNTPPermanentError(resp
)
159 if readermode_afterauth
:
161 self
.welcome
= self
.shortcmd('mode reader')
162 except NNTPPermanentError
:
163 # error 500, probably 'not implemented'
167 # 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
172 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."""
178 if self
.debugging
: print '*welcome*', repr(self
.welcome
)
181 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"""
187 self
.debugging
= level
188 debug
= set_debuglevel
190 def putline(self
, line
):
191 """Internal: send one line to the server, appending CRLF."""
193 if self
.debugging
> 1: print '*put*', repr(line
)
194 self
.sock
.sendall(line
)
196 def putcmd(self
, line
):
197 """Internal: send one command to the server (through putline())."""
198 if self
.debugging
: print '*cmd*', repr(line
)
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:
206 print '*get*', repr(line
)
207 if not line
: raise EOFError
208 if line
[-2:] == CRLF
: line
= line
[:-2]
209 elif line
[-1:] in CRLF
: line
= line
[:-1]
213 """Internal: get a response from the server.
214 Raise various errors if the response indicates an error."""
215 resp
= self
.getline()
216 if self
.debugging
: print '*resp*', repr(resp
)
219 raise NNTPTemporaryError(resp
)
221 raise NNTPPermanentError(resp
)
223 raise NNTPProtocolError(resp
)
226 def getlongresp(self
, file=None):
227 """Internal: get a response plus following text from the server.
228 Raise various errors if the response indicates an error."""
232 # If a string was passed then open a file with that name
233 if isinstance(file, str):
234 openedFile
= file = open(file, "w")
236 resp
= self
.getresp()
237 if resp
[:3] not in LONGRESP
:
238 raise NNTPReplyError(resp
)
241 line
= self
.getline()
247 file.write(line
+ "\n")
251 # If this method created the file, then it must close it
257 def shortcmd(self
, line
):
258 """Internal: send a command and get the response."""
260 return self
.getresp()
262 def longcmd(self
, line
, file=None):
263 """Internal: send a command and get the response plus following text."""
265 return self
.getlongresp(file)
267 def newgroups(self
, date
, time
, file=None):
268 """Process a NEWGROUPS command. Arguments:
269 - date: string 'yymmdd' indicating the date
270 - time: string 'hhmmss' indicating the time
272 - resp: server response if successful
273 - list: list of newsgroup names"""
275 return self
.longcmd('NEWGROUPS ' + date
+ ' ' + time
, file)
277 def newnews(self
, group
, date
, time
, file=None):
278 """Process a NEWNEWS command. Arguments:
279 - group: group name or '*'
280 - date: string 'yymmdd' indicating the date
281 - time: string 'hhmmss' indicating the time
283 - resp: server response if successful
284 - list: list of message ids"""
286 cmd
= 'NEWNEWS ' + group
+ ' ' + date
+ ' ' + time
287 return self
.longcmd(cmd
, file)
289 def list(self
, file=None):
290 """Process a LIST command. Return:
291 - resp: server response if successful
292 - list: list of (group, last, first, flag) (strings)"""
294 resp
, list = self
.longcmd('LIST', file)
295 for i
in range(len(list)):
296 # Parse lines into "group last first flag"
297 list[i
] = tuple(list[i
].split())
300 def description(self
, group
):
302 """Get a description for a single group. If more than one
303 group matches ('group' is a pattern), return the first. If no
304 group matches, return an empty string.
306 This elides the response code from the server, since it can
307 only be '215' or '285' (for xgtitle) anyway. If the response
308 code is needed, use the 'descriptions' method.
310 NOTE: This neither checks for a wildcard in 'group' nor does
311 it check whether the group actually exists."""
313 resp
, lines
= self
.descriptions(group
)
319 def descriptions(self
, group_pattern
):
320 """Get descriptions for a range of groups."""
321 line_pat
= re
.compile("^(?P<group>[^ \t]+)[ \t]+(.*)$")
322 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
323 resp
, raw_lines
= self
.longcmd('LIST NEWSGROUPS ' + group_pattern
)
324 if resp
[:3] != "215":
325 # Now the deprecated XGTITLE. This either raises an error
326 # or succeeds with the same output structure as LIST
328 resp
, raw_lines
= self
.longcmd('XGTITLE ' + group_pattern
)
330 for raw_line
in raw_lines
:
331 match
= line_pat
.search(raw_line
.strip())
333 lines
.append(match
.group(1, 2))
336 def group(self
, name
):
337 """Process a GROUP command. Argument:
338 - group: the group name
340 - resp: server response if successful
341 - count: number of articles (string)
342 - first: first article number (string)
343 - last: last article number (string)
344 - name: the group name"""
346 resp
= self
.shortcmd('GROUP ' + name
)
347 if resp
[:3] != '211':
348 raise NNTPReplyError(resp
)
350 count
= first
= last
= 0
359 name
= words
[4].lower()
360 return resp
, count
, first
, last
, name
362 def help(self
, file=None):
363 """Process a HELP command. Returns:
364 - resp: server response if successful
365 - list: list of strings"""
367 return self
.longcmd('HELP',file)
369 def statparse(self
, resp
):
370 """Internal: parse the response of a STAT, NEXT or LAST command."""
372 raise NNTPReplyError(resp
)
383 def statcmd(self
, line
):
384 """Internal: process a STAT, NEXT or LAST command."""
385 resp
= self
.shortcmd(line
)
386 return self
.statparse(resp
)
389 """Process a STAT command. Argument:
390 - id: article number or message id
392 - resp: server response if successful
393 - nr: the article number
394 - id: the message id"""
396 return self
.statcmd('STAT ' + id)
399 """Process a NEXT command. No arguments. Return as for STAT."""
400 return self
.statcmd('NEXT')
403 """Process a LAST command. No arguments. Return as for STAT."""
404 return self
.statcmd('LAST')
406 def artcmd(self
, line
, file=None):
407 """Internal: process a HEAD, BODY or ARTICLE command."""
408 resp
, list = self
.longcmd(line
, file)
409 resp
, nr
, id = self
.statparse(resp
)
410 return resp
, nr
, id, list
413 """Process a HEAD command. Argument:
414 - id: article number or message id
416 - resp: server response if successful
419 - list: the lines of the article's header"""
421 return self
.artcmd('HEAD ' + id)
423 def body(self
, id, file=None):
424 """Process a BODY command. Argument:
425 - id: article number or message id
426 - file: Filename string or file object to store the article in
428 - resp: server response if successful
431 - list: the lines of the article's body or an empty list
434 return self
.artcmd('BODY ' + id, file)
436 def article(self
, id):
437 """Process an ARTICLE command. Argument:
438 - id: article number or message id
440 - resp: server response if successful
443 - list: the lines of the article"""
445 return self
.artcmd('ARTICLE ' + id)
448 """Process a SLAVE command. Returns:
449 - resp: server response if successful"""
451 return self
.shortcmd('SLAVE')
453 def xhdr(self
, hdr
, str, file=None):
454 """Process an XHDR command (optional server extension). Arguments:
455 - hdr: the header type (e.g. 'subject')
456 - str: an article nr, a message id, or a range nr1-nr2
458 - resp: server response if successful
459 - list: list of (nr, value) strings"""
461 pat
= re
.compile('^([0-9]+) ?(.*)\n?')
462 resp
, lines
= self
.longcmd('XHDR ' + hdr
+ ' ' + str, file)
463 for i
in range(len(lines
)):
467 lines
[i
] = m
.group(1, 2)
470 def xover(self
, start
, end
, file=None):
471 """Process an XOVER command (optional server extension) Arguments:
472 - start: start of range
475 - resp: server response if successful
476 - list: list of (art-nr, subject, poster, date,
477 id, references, size, lines)"""
479 resp
, lines
= self
.longcmd('XOVER ' + start
+ '-' + end
, file)
482 elem
= line
.split("\t")
484 xover_lines
.append((elem
[0],
493 raise NNTPDataError(line
)
494 return resp
,xover_lines
496 def xgtitle(self
, group
, file=None):
497 """Process an XGTITLE command (optional server extension) Arguments:
498 - group: group name wildcard (i.e. news.*)
500 - resp: server response if successful
501 - list: list of (name,title) strings"""
503 line_pat
= re
.compile("^([^ \t]+)[ \t]+(.*)$")
504 resp
, raw_lines
= self
.longcmd('XGTITLE ' + group
, file)
506 for raw_line
in raw_lines
:
507 match
= line_pat
.search(raw_line
.strip())
509 lines
.append(match
.group(1, 2))
513 """Process an XPATH command (optional server extension) Arguments:
514 - id: Message id of article
516 resp: server response if successful
517 path: directory path to article"""
519 resp
= self
.shortcmd("XPATH " + id)
520 if resp
[:3] != '223':
521 raise NNTPReplyError(resp
)
523 [resp_num
, path
] = resp
.split()
525 raise NNTPReplyError(resp
)
530 """Process the DATE command. Arguments:
533 resp: server response if successful
534 date: Date suitable for newnews/newgroups commands etc.
535 time: Time suitable for newnews/newgroups commands etc."""
537 resp
= self
.shortcmd("DATE")
538 if resp
[:3] != '111':
539 raise NNTPReplyError(resp
)
542 raise NNTPDataError(resp
)
545 if len(date
) != 6 or len(time
) != 6:
546 raise NNTPDataError(resp
)
547 return resp
, date
, time
551 """Process a POST command. Arguments:
552 - f: file containing the article
554 - resp: server response if successful"""
556 resp
= self
.shortcmd('POST')
557 # Raises error_??? if posting is not allowed
559 raise NNTPReplyError(resp
)
570 return self
.getresp()
572 def ihave(self
, id, f
):
573 """Process an IHAVE command. Arguments:
574 - id: message-id of the article
575 - f: file containing the article
577 - resp: server response if successful
578 Note that if the server refuses the article an exception is raised."""
580 resp
= self
.shortcmd('IHAVE ' + id)
581 # Raises error_??? if the server already has it
583 raise NNTPReplyError(resp
)
594 return self
.getresp()
597 """Process a QUIT command and close the socket. Returns:
598 - resp: server response if successful"""
600 resp
= self
.shortcmd('QUIT')
603 del self
.file, self
.sock
607 # Test retrieval when run as a script.
608 # Assumption: if there's a local news server, it's called 'news'.
609 # Assumption: if user queries a remote news server, it's named
610 # in the environment variable NNTPSERVER (used by slrn and kin)
611 # and we want readermode off.
612 if __name__
== '__main__':
614 newshost
= 'news' and os
.environ
["NNTPSERVER"]
615 if newshost
.find('.') == -1:
619 s
= NNTP(newshost
, readermode
=mode
)
620 resp
, count
, first
, last
, name
= s
.group('comp.lang.python')
622 print 'Group', name
, 'has', count
, 'articles, range', first
, 'to', last
623 resp
, subs
= s
.xhdr('subject', first
+ '-' + last
)
626 print "%7s %s" % item