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
):
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 # Get the welcome message from the server
76 # (this is read and squirreled away by __init__()).
77 # If the response code is 200, posting is allowed;
78 # if it 201, posting is not allowed
81 if self
.debugging
: print '*welcome*', `self
.welcome`
84 # Set the debugging level. Argument level means:
85 # 0: no debugging output (default)
86 # 1: print commands and responses but not body text etc.
87 # 2: also print raw lines read and sent before stripping CR/LF
89 def set_debuglevel(self
, level
):
90 self
.debugging
= level
91 debug
= set_debuglevel
93 # Internal: send one line to the server, appending CRLF
94 def putline(self
, line
):
96 if self
.debugging
> 1: print '*put*', `line`
99 # Internal: send one command to the server (through putline())
100 def putcmd(self
, line
):
101 if self
.debugging
: print '*cmd*', `line`
104 # Internal: return one line from the server, stripping CRLF.
105 # Raise EOFError if the connection is closed
107 line
= self
.file.readline()
108 if self
.debugging
> 1:
109 print '*get*', `line`
110 if not line
: raise EOFError
111 if line
[-2:] == CRLF
: line
= line
[:-2]
112 elif line
[-1:] in CRLF
: line
= line
[:-1]
115 # Internal: get a response from the server.
116 # Raise various errors if the response indicates an error
118 resp
= self
.getline()
119 if self
.debugging
: print '*resp*', `resp`
122 raise error_temp
, resp
124 raise error_perm
, resp
126 raise error_proto
, resp
129 # Internal: get a response plus following text from the server.
130 # Raise various errors if the response indicates an error
131 def getlongresp(self
):
132 resp
= self
.getresp()
133 if resp
[:3] not in LONGRESP
:
134 raise error_reply
, resp
137 line
= self
.getline()
143 # Internal: send a command and get the response
144 def shortcmd(self
, line
):
146 return self
.getresp()
148 # Internal: send a command and get the response plus following text
149 def longcmd(self
, line
):
151 return self
.getlongresp()
153 # Process a NEWGROUPS command. Arguments:
154 # - date: string 'yymmdd' indicating the date
155 # - time: string 'hhmmss' indicating the time
157 # - resp: server response if succesful
158 # - list: list of newsgroup names
160 def newgroups(self
, date
, time
):
161 return self
.longcmd('NEWGROUPS ' + date
+ ' ' + time
)
163 # Process a NEWNEWS command. Arguments:
164 # - group: group name or '*'
165 # - date: string 'yymmdd' indicating the date
166 # - time: string 'hhmmss' indicating the time
168 # - resp: server response if succesful
169 # - list: list of article ids
171 def newnews(self
, group
, date
, time
):
172 cmd
= 'NEWNEWS ' + group
+ ' ' + date
+ ' ' + time
173 return self
.longcmd(cmd
)
175 # Process a LIST command. Return:
176 # - resp: server response if succesful
177 # - list: list of (group, last, first, flag) (strings)
180 resp
, list = self
.longcmd('LIST')
181 for i
in range(len(list)):
182 # Parse lines into "group last first flag"
183 list[i
] = string
.split(list[i
])
186 # Process a GROUP command. Argument:
187 # - group: the group name
189 # - resp: server response if succesful
190 # - count: number of articles (string)
191 # - first: first article number (string)
192 # - last: last article number (string)
193 # - name: the group name
195 def group(self
, name
):
196 resp
= self
.shortcmd('GROUP ' + name
)
197 if resp
[:3] <> '211':
198 raise error_reply
, resp
199 words
= string
.split(resp
)
200 count
= first
= last
= 0
209 name
= string
.lower(words
[4])
210 return resp
, count
, first
, last
, name
212 # Process a HELP command. Returns:
213 # - resp: server response if succesful
214 # - list: list of strings
217 return self
.longcmd('HELP')
219 # Internal: parse the response of a STAT, NEXT or LAST command
220 def statparse(self
, resp
):
222 raise error_reply
, resp
223 words
= string
.split(resp
)
230 id = string
.lower(words
[2])
233 # Internal: process a STAT, NEXT or LAST command
234 def statcmd(self
, line
):
235 resp
= self
.shortcmd(line
)
236 return self
.statparse(resp
)
238 # Process a STAT command. Argument:
239 # - id: article number or message id
241 # - resp: server response if succesful
242 # - nr: the article number
243 # - id: the article id
246 return self
.statcmd('STAT ' + id)
248 # Process a NEXT command. No arguments. Return as for STAT
251 return self
.statcmd('NEXT')
253 # Process a LAST command. No arguments. Return as for STAT
256 return self
.statcmd('LAST')
258 # Internal: process a HEAD, BODY or ARTICLE command
259 def artcmd(self
, line
):
260 resp
, list = self
.longcmd(line
)
261 resp
, nr
, id = self
.statparse(resp
)
262 return resp
, nr
, id, list
264 # Process a HEAD command. Argument:
265 # - id: article number or message id
267 # - resp: server response if succesful
268 # - list: the lines of the article's header
271 return self
.artcmd('HEAD ' + id)
273 # Process a BODY command. Argument:
274 # - id: article number or message id
276 # - resp: server response if succesful
277 # - list: the lines of the article's body
280 return self
.artcmd('BODY ' + id)
282 # Process an ARTICLE command. Argument:
283 # - id: article number or message id
285 # - resp: server response if succesful
286 # - list: the lines of the article
288 def article(self
, id):
289 return self
.artcmd('ARTICLE ' + id)
291 # Process a SLAVE command. Returns:
292 # - resp: server response if succesful
295 return self
.shortcmd('SLAVE')
297 # Process an XHDR command (optional server extension). Arguments:
298 # - hdr: the header type (e.g. 'subject')
299 # - str: an article nr, a message id, or a range nr1-nr2
301 # - resp: server response if succesful
302 # - list: list of (nr, value) strings
304 def xhdr(self
, hdr
, str):
305 resp
, lines
= self
.longcmd('XHDR ' + hdr
+ ' ' + str)
306 for i
in range(len(lines
)):
308 n
= regex
.match('^[0-9]+', line
)
310 if n
< len(line
) and line
[n
] == ' ': n
= n
+1
311 lines
[i
] = (nr
, line
[n
:])
314 # Process an XOVER command (optional server extension) Arguments:
315 # - start: start of range
316 # - end: end of range
318 # - resp: server response if succesful
319 # - list: list of (art-nr, subject, poster, date, id, refrences, size, lines)
321 def xover(self
,start
,end
):
322 resp
, lines
= self
.longcmd('XOVER ' + start
+ '-' + end
)
325 elem
= string
.splitfields(line
,"\t")
327 xover_lines
.append(elem
[0],
336 raise error_data
,line
337 return resp
,xover_lines
339 # Process an XGTITLE command (optional server extension) Arguments:
340 # - group: group name wildcard (i.e. news.*)
342 # - resp: server response if succesful
343 # - list: list of (name,title) strings
345 def xgtitle(self
, group
):
346 line_pat
= regex
.compile("^\([^ \t]+\)[ \t]+\(.*\)$")
347 resp
, raw_lines
= self
.longcmd('XGTITLE ' + group
)
349 for raw_line
in raw_lines
:
350 if line_pat
.search(string
.strip(raw_line
)) == 0:
351 lines
.append(line_pat
.group(1),
356 # Process an XPATH command (optional server extension) Arguments:
357 # - id: Message id of article
359 # resp: server response if succesful
360 # path: directory path to article
363 resp
= self
.shortcmd("XPATH " + id)
364 if resp
[:3] <> '223':
365 raise error_reply
, resp
367 [resp_num
, path
] = string
.split(resp
)
369 raise error_reply
, resp
373 # Process the DATE command. Arguments:
376 # resp: server response if succesful
377 # date: Date suitable for newnews/newgroups commands etc.
378 # time: Time suitable for newnews/newgroups commands etc.
381 resp
= self
.shortcmd("DATE")
382 if resp
[:3] <> '111':
383 raise error_reply
, resp
384 elem
= string
.split(resp
)
386 raise error_data
, resp
389 if len(date
) != 6 or len(time
) != 6:
390 raise error_data
, resp
391 return resp
, date
, time
394 # Process a POST command. Arguments:
395 # - f: file containing the article
397 # - resp: server response if succesful
400 resp
= self
.shortcmd('POST')
401 # Raises error_??? if posting is not allowed
403 raise error_reply
, resp
414 return self
.getresp()
416 # Process an IHAVE command. Arguments:
417 # - id: message-id of the article
418 # - f: file containing the article
420 # - resp: server response if succesful
421 # Note that if the server refuses the article an exception is raised
423 def ihave(self
, id, f
):
424 resp
= self
.shortcmd('IHAVE ' + id)
425 # Raises error_??? if the server already has it
427 raise error_reply
, resp
438 return self
.getresp()
440 # Process a QUIT command and close the socket. Returns:
441 # - resp: server response if succesful
444 resp
= self
.shortcmd('QUIT')
447 del self
.file, self
.sock