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
8 # >>> s = NNTP('charon')
9 # >>> resp, count, first, last, name = s.group('nlnet.misc')
10 # >>> print 'Group', name, 'has', count, 'articles, range', first, 'to', last
11 # Group nlnet.misc has 525 articles, range 6960 to 7485
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.
35 # Exception raised when an error or invalid response is received
37 error_reply
= 'nntplib.error_reply' # unexpected [123]xx reply
38 error_temp
= 'nntplib.error_temp' # 4xx errors
39 error_perm
= 'nntplib.error_perm' # 5xx errors
40 error_proto
= 'nntplib.error_proto' # response does not begin with [1-5]
43 # Standard port used by NNTP servers
47 # Response numbers that are followed by additional text (e.g. article)
48 LONGRESP
= ['100', '215', '220', '221', '222', '230', '231']
51 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
59 # Initialize an instance. Arguments:
60 # - host: hostname to connect to
61 # - port: port to connect to (default the standard NNTP port)
63 def __init__(self
, host
, port
= NNTP_PORT
):
66 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
67 self
.sock
.connect(self
.host
, self
.port
)
68 self
.file = self
.sock
.makefile('r')
70 self
.welcome
= self
.getresp()
72 # Get the welcome message from the server
73 # (this is read and squirreled away by __init__()).
74 # If the response code is 200, posting is allowed;
75 # if it 201, posting is not allowed
78 if self
.debugging
: print '*welcome*', `self
.welcome`
81 # Set the debugging level. Argument level means:
82 # 0: no debugging output (default)
83 # 1: print commands and responses but not body text etc.
84 # 2: also print raw lines read and sent before stripping CR/LF
86 def debug(self
, level
):
87 self
.debugging
= level
89 # Internal: send one line to the server, appending CRLF
90 def putline(self
, line
):
92 if self
.debugging
> 1: print '*put*', `line`
95 # Internal: send one command to the server (through putline())
96 def putcmd(self
, line
):
97 if self
.debugging
: print '*cmd*', `line`
100 # Internal: return one line from the server, stripping CRLF.
101 # Raise EOFError if the connection is closed
103 line
= self
.file.readline()
104 if self
.debugging
> 1:
105 print '*get*', `line`
106 if not line
: raise EOFError
107 if line
[-2:] == CRLF
: line
= line
[:-2]
108 elif line
[-1:] in CRLF
: line
= line
[:-1]
111 # Internal: get a response from the server.
112 # Raise various errors if the response indicates an error
114 resp
= self
.getline()
115 if self
.debugging
: print '*resp*', `resp`
118 raise error_temp
, resp
120 raise error_perm
, resp
122 raise error_proto
, resp
125 # Internal: get a response plus following text from the server.
126 # Raise various errors if the response indicates an error
127 def getlongresp(self
):
128 resp
= self
.getresp()
129 if resp
[:3] not in LONGRESP
:
130 raise error_reply
, resp
133 line
= self
.getline()
139 # Internal: send a command and get the response
140 def shortcmd(self
, line
):
142 return self
.getresp()
144 # Internal: send a command and get the response plus following text
145 def longcmd(self
, line
):
147 return self
.getlongresp()
149 # Process a NEWGROUPS command. Arguments:
150 # - date: string 'yymmdd' indicating the date
151 # - time: string 'hhmmss' indicating the time
153 # - resp: server response if succesful
154 # - list: list of newsgroup names
156 def newgroups(self
, date
, time
):
157 return self
.longcmd('NEWGROUPS ' + date
+ ' ' + time
)
159 # Process a NEWNEWS command. Arguments:
160 # - group: group name or '*'
161 # - date: string 'yymmdd' indicating the date
162 # - time: string 'hhmmss' indicating the time
164 # - resp: server response if succesful
165 # - list: list of article ids
167 def newnews(self
, group
, date
, time
):
168 cmd
= 'NEWNEWS ' + group
+ ' ' + date
+ ' ' + time
169 return self
.longcmd(cmd
)
171 # Process a LIST command. Return:
172 # - resp: server response if succesful
173 # - list: list of (group, last, first, flag) (strings)
176 resp
, list = self
.longcmd('LIST')
177 for i
in range(len(list)):
178 # Parse lines into "group last first flag"
179 list[i
] = string
.split(list[i
])
182 # Process a GROUP command. Argument:
183 # - group: the group name
185 # - resp: server response if succesful
186 # - count: number of articles (string)
187 # - first: first article number (string)
188 # - last: last article number (string)
189 # - name: the group name
191 def group(self
, name
):
192 resp
= self
.shortcmd('GROUP ' + name
)
193 if resp
[:3] <> '211':
194 raise error_reply
, resp
195 words
= string
.split(resp
)
196 count
= first
= last
= 0
205 name
= string
.lower(words
[4])
206 return resp
, count
, first
, last
, name
208 # Process a HELP command. Returns:
209 # - resp: server response if succesful
210 # - list: list of strings
213 return self
.longcmd('HELP')
215 # Internal: parse the response of a STAT, NEXT or LAST command
216 def statparse(self
, resp
):
218 raise error_reply
, resp
219 words
= string
.split(resp
)
226 id = string
.lower(words
[2])
229 # Internal: process a STAT, NEXT or LAST command
230 def statcmd(self
, line
):
231 resp
= self
.shortcmd(line
)
232 return self
.statparse(resp
)
234 # Process a STAT command. Argument:
235 # - id: article number or message id
237 # - resp: server response if succesful
238 # - nr: the article number
239 # - id: the article id
242 return self
.statcmd('STAT ' + id)
244 # Process a NEXT command. No arguments. Return as for STAT
247 return self
.statcmd('NEXT')
249 # Process a LAST command. No arguments. Return as for STAT
252 return self
.statcmd('LAST')
254 # Internal: process a HEAD, BODY or ARTICLE command
255 def artcmd(self
, line
):
256 resp
, list = self
.longcmd(line
)
257 resp
, nr
, id = self
.statparse(resp
)
258 return resp
, nr
, id, list
260 # Process a HEAD command. Argument:
261 # - id: article number or message id
263 # - resp: server response if succesful
264 # - list: the lines of the article's header
267 return self
.artcmd('HEAD ' + id)
269 # Process a BODY command. Argument:
270 # - id: article number or message id
272 # - resp: server response if succesful
273 # - list: the lines of the article's body
276 return self
.artcmd('BODY ' + id)
278 # Process an ARTICLE command. Argument:
279 # - id: article number or message id
281 # - resp: server response if succesful
282 # - list: the lines of the article
284 def article(self
, id):
285 return self
.artcmd('ARTICLE ' + id)
287 # Process a SLAVE command. Returns:
288 # - resp: server response if succesful
291 return self
.shortcmd('SLAVE')
293 # Process an XHDR command (optional server extension). Arguments:
294 # - hdr: the header type (e.g. 'subject')
295 # - str: an article nr, a message id, or a range nr1-nr2
297 # - resp: server response if succesful
298 # - list: list of (nr, value) strings
300 def xhdr(self
, hdr
, str):
301 resp
, lines
= self
.longcmd('XHDR ' + hdr
+ ' ' + str)
302 for i
in range(len(lines
)):
304 n
= regex
.match('^[0-9]+', line
)
306 if n
< len(line
) and line
[n
] == ' ': n
= n
+1
307 lines
[i
] = (nr
, line
[n
:])
310 # Process a POST command. Arguments:
311 # - f: file containing the article
313 # - resp: server response if succesful
316 resp
= self
.shortcmd('POST')
317 # Raises error_??? if posting is not allowed
319 raise error_reply
, resp
330 return self
.getresp()
332 # Process an IHAVE command. Arguments:
333 # - id: message-id of the article
334 # - f: file containing the article
336 # - resp: server response if succesful
337 # Note that if the server refuses the article an exception is raised
339 def ihave(self
, id, f
):
340 resp
= self
.shortcmd('IHAVE ' + id)
341 # Raises error_??? if the server already has it
343 raise error_reply
, resp
354 return self
.getresp()
356 # Process a QUIT command and close the socket. Returns:
357 # - resp: server response if succesful
360 resp
= self
.shortcmd('QUIT')
363 del self
.file, self
.sock