1 # An NNTP client class. Based on RFC 977: Network News Transfer
2 # Protocol, by Brian Kantor and Phil Lapsley.
7 # >>> from nntplib import NNTP
9 # >>> resp, count, first, last, name = s.group('comp.lang.python')
10 # >>> print 'Group', name, 'has', count, 'articles, range', first, 'to', last
11 # Group comp.lang.python has 51 articles, range 5770 to 5821
12 # >>> resp, subs = s.xhdr('subject', first + '-' + last)
16 # Here 'resp' is the server response line.
17 # Error responses are turned into exceptions.
19 # To post an article from a file:
20 # >>> f = open(filename, 'r') # file containing article, including header
21 # >>> resp = s.post(f)
24 # For descriptions of all methods, read the comments in the code below.
25 # Note that all arguments and return values representing article numbers
26 # are strings, not numbers, since they are rarely used for calculations.
28 # (xover, xgtitle, xpath, date methods by Kevan Heydon)
37 # Exception raised when an error or invalid response is received
39 error_reply
= 'nntplib.error_reply' # unexpected [123]xx reply
40 error_temp
= 'nntplib.error_temp' # 4xx errors
41 error_perm
= 'nntplib.error_perm' # 5xx errors
42 error_proto
= 'nntplib.error_proto' # response does not begin with [1-5]
43 error_data
= 'nntplib.error_data' # error in response data
46 # Standard port used by NNTP servers
50 # Response numbers that are followed by additional text (e.g. article)
51 LONGRESP
= ['100', '215', '220', '221', '222', '224', '230', '231', '282']
54 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
62 # Initialize an instance. Arguments:
63 # - host: hostname to connect to
64 # - port: port to connect to (default the standard NNTP port)
66 def __init__(self
, host
, port
= NNTP_PORT
, user
=None, password
=None):
69 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
70 self
.sock
.connect(self
.host
, self
.port
)
71 self
.file = self
.sock
.makefile('rb')
73 self
.welcome
= self
.getresp()
75 resp
= self
.shortcmd('authinfo user '+user
)
78 raise error_reply
, resp
81 'authinfo pass '+password
)
83 raise error_perm
, resp
85 # Get the welcome message from the server
86 # (this is read and squirreled away by __init__()).
87 # If the response code is 200, posting is allowed;
88 # if it 201, posting is not allowed
91 if self
.debugging
: print '*welcome*', `self
.welcome`
94 # Set the debugging level. Argument level means:
95 # 0: no debugging output (default)
96 # 1: print commands and responses but not body text etc.
97 # 2: also print raw lines read and sent before stripping CR/LF
99 def set_debuglevel(self
, level
):
100 self
.debugging
= level
101 debug
= set_debuglevel
103 # Internal: send one line to the server, appending CRLF
104 def putline(self
, line
):
106 if self
.debugging
> 1: print '*put*', `line`
109 # Internal: send one command to the server (through putline())
110 def putcmd(self
, line
):
111 if self
.debugging
: print '*cmd*', `line`
114 # Internal: return one line from the server, stripping CRLF.
115 # Raise EOFError if the connection is closed
117 line
= self
.file.readline()
118 if self
.debugging
> 1:
119 print '*get*', `line`
120 if not line
: raise EOFError
121 if line
[-2:] == CRLF
: line
= line
[:-2]
122 elif line
[-1:] in CRLF
: line
= line
[:-1]
125 # Internal: get a response from the server.
126 # Raise various errors if the response indicates an error
128 resp
= self
.getline()
129 if self
.debugging
: print '*resp*', `resp`
132 raise error_temp
, resp
134 raise error_perm
, resp
136 raise error_proto
, resp
139 # Internal: get a response plus following text from the server.
140 # Raise various errors if the response indicates an error
141 def getlongresp(self
):
142 resp
= self
.getresp()
143 if resp
[:3] not in LONGRESP
:
144 raise error_reply
, resp
147 line
= self
.getline()
155 # Internal: send a command and get the response
156 def shortcmd(self
, line
):
158 return self
.getresp()
160 # Internal: send a command and get the response plus following text
161 def longcmd(self
, line
):
163 return self
.getlongresp()
165 # Process a NEWGROUPS command. Arguments:
166 # - date: string 'yymmdd' indicating the date
167 # - time: string 'hhmmss' indicating the time
169 # - resp: server response if succesful
170 # - list: list of newsgroup names
172 def newgroups(self
, date
, time
):
173 return self
.longcmd('NEWGROUPS ' + date
+ ' ' + time
)
175 # Process a NEWNEWS command. Arguments:
176 # - group: group name or '*'
177 # - date: string 'yymmdd' indicating the date
178 # - time: string 'hhmmss' indicating the time
180 # - resp: server response if succesful
181 # - list: list of article ids
183 def newnews(self
, group
, date
, time
):
184 cmd
= 'NEWNEWS ' + group
+ ' ' + date
+ ' ' + time
185 return self
.longcmd(cmd
)
187 # Process a LIST command. Return:
188 # - resp: server response if succesful
189 # - list: list of (group, last, first, flag) (strings)
192 resp
, list = self
.longcmd('LIST')
193 for i
in range(len(list)):
194 # Parse lines into "group last first flag"
195 list[i
] = tuple(string
.split(list[i
]))
198 # Process a GROUP command. Argument:
199 # - group: the group name
201 # - resp: server response if succesful
202 # - count: number of articles (string)
203 # - first: first article number (string)
204 # - last: last article number (string)
205 # - name: the group name
207 def group(self
, name
):
208 resp
= self
.shortcmd('GROUP ' + name
)
209 if resp
[:3] <> '211':
210 raise error_reply
, resp
211 words
= string
.split(resp
)
212 count
= first
= last
= 0
221 name
= string
.lower(words
[4])
222 return resp
, count
, first
, last
, name
224 # Process a HELP command. Returns:
225 # - resp: server response if succesful
226 # - list: list of strings
229 return self
.longcmd('HELP')
231 # Internal: parse the response of a STAT, NEXT or LAST command
232 def statparse(self
, resp
):
234 raise error_reply
, resp
235 words
= string
.split(resp
)
245 # Internal: process a STAT, NEXT or LAST command
246 def statcmd(self
, line
):
247 resp
= self
.shortcmd(line
)
248 return self
.statparse(resp
)
250 # Process a STAT command. Argument:
251 # - id: article number or message id
253 # - resp: server response if succesful
254 # - nr: the article number
255 # - id: the article id
258 return self
.statcmd('STAT ' + id)
260 # Process a NEXT command. No arguments. Return as for STAT
263 return self
.statcmd('NEXT')
265 # Process a LAST command. No arguments. Return as for STAT
268 return self
.statcmd('LAST')
270 # Internal: process a HEAD, BODY or ARTICLE command
271 def artcmd(self
, line
):
272 resp
, list = self
.longcmd(line
)
273 resp
, nr
, id = self
.statparse(resp
)
274 return resp
, nr
, id, list
276 # Process a HEAD command. Argument:
277 # - id: article number or message id
279 # - resp: server response if succesful
280 # - nr: article number
282 # - list: the lines of the article's header
285 return self
.artcmd('HEAD ' + id)
287 # Process a BODY command. Argument:
288 # - id: article number or message id
290 # - resp: server response if succesful
291 # - nr: article number
293 # - list: the lines of the article's body
296 return self
.artcmd('BODY ' + id)
298 # Process an ARTICLE command. Argument:
299 # - id: article number or message id
301 # - resp: server response if succesful
302 # - nr: article number
304 # - list: the lines of the article
306 def article(self
, id):
307 return self
.artcmd('ARTICLE ' + id)
309 # Process a SLAVE command. Returns:
310 # - resp: server response if succesful
313 return self
.shortcmd('SLAVE')
315 # Process an XHDR command (optional server extension). Arguments:
316 # - hdr: the header type (e.g. 'subject')
317 # - str: an article nr, a message id, or a range nr1-nr2
319 # - resp: server response if succesful
320 # - list: list of (nr, value) strings
322 def xhdr(self
, hdr
, str):
323 pat
= re
.compile('^([0-9]+) ?(.*)\n?')
324 resp
, lines
= self
.longcmd('XHDR ' + hdr
+ ' ' + str)
325 for i
in range(len(lines
)):
329 lines
[i
] = m
.group(1, 2)
332 # Process an XOVER command (optional server extension) Arguments:
333 # - start: start of range
334 # - end: end of range
336 # - resp: server response if succesful
337 # - list: list of (art-nr, subject, poster, date, id, refrences, size, lines)
339 def xover(self
,start
,end
):
340 resp
, lines
= self
.longcmd('XOVER ' + start
+ '-' + end
)
343 elem
= string
.splitfields(line
,"\t")
345 xover_lines
.append((elem
[0],
350 string
.split(elem
[5]),
354 raise error_data
,line
355 return resp
,xover_lines
357 # Process an XGTITLE command (optional server extension) Arguments:
358 # - group: group name wildcard (i.e. news.*)
360 # - resp: server response if succesful
361 # - list: list of (name,title) strings
363 def xgtitle(self
, group
):
364 line_pat
= re
.compile("^([^ \t]+)[ \t]+(.*)$")
365 resp
, raw_lines
= self
.longcmd('XGTITLE ' + group
)
367 for raw_line
in raw_lines
:
368 match
= line_pat
.search(string
.strip(raw_line
))
370 lines
.append(match
.group(1, 2))
373 # Process an XPATH command (optional server extension) Arguments:
374 # - id: Message id of article
376 # resp: server response if succesful
377 # path: directory path to article
380 resp
= self
.shortcmd("XPATH " + id)
381 if resp
[:3] <> '223':
382 raise error_reply
, resp
384 [resp_num
, path
] = string
.split(resp
)
386 raise error_reply
, resp
390 # Process the DATE command. Arguments:
393 # resp: server response if succesful
394 # date: Date suitable for newnews/newgroups commands etc.
395 # time: Time suitable for newnews/newgroups commands etc.
398 resp
= self
.shortcmd("DATE")
399 if resp
[:3] <> '111':
400 raise error_reply
, resp
401 elem
= string
.split(resp
)
403 raise error_data
, resp
406 if len(date
) != 6 or len(time
) != 6:
407 raise error_data
, resp
408 return resp
, date
, time
411 # Process a POST command. Arguments:
412 # - f: file containing the article
414 # - resp: server response if succesful
417 resp
= self
.shortcmd('POST')
418 # Raises error_??? if posting is not allowed
420 raise error_reply
, resp
431 return self
.getresp()
433 # Process an IHAVE command. Arguments:
434 # - id: message-id of the article
435 # - f: file containing the article
437 # - resp: server response if succesful
438 # Note that if the server refuses the article an exception is raised
440 def ihave(self
, id, f
):
441 resp
= self
.shortcmd('IHAVE ' + id)
442 # Raises error_??? if the server already has it
444 raise error_reply
, resp
455 return self
.getresp()
457 # Process a QUIT command and close the socket. Returns:
458 # - resp: server response if succesful
461 resp
= self
.shortcmd('QUIT')
464 del self
.file, self
.sock
468 # Minimal test function
471 resp
, count
, first
, last
, name
= s
.group('comp.lang.python')
473 print 'Group', name
, 'has', count
, 'articles, range', first
, 'to', last
474 resp
, subs
= s
.xhdr('subject', first
+ '-' + last
)
477 print "%7s %s" % item
482 # Run the test when run as a script
483 if __name__
== '__main__':