Vega strike Python Modules doc  0.5.1
Documentation of the " Modules " folder of Vega strike
 All Data Structures Namespaces Files Functions Variables
nntplib.py
Go to the documentation of this file.
1 """An NNTP client class based on RFC 977: Network News Transfer Protocol.
2 
3 Example:
4 
5 >>> from nntplib import NNTP
6 >>> s = NNTP('news')
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)
11 >>> resp = s.quit()
12 >>>
13 
14 Here 'resp' is the server response line.
15 Error responses are turned into exceptions.
16 
17 To post an article from a file:
18 >>> f = open(filename, 'r') # file containing article, including header
19 >>> resp = s.post(f)
20 >>>
21 
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.
25 """
26 
27 # RFC 977 by Brian Kantor and Phil Lapsley.
28 # xover, xgtitle, xpath, date methods by Kevan Heydon
29 
30 
31 # Imports
32 import re
33 import socket
34 import types
35 
36 __all__ = ["NNTP","NNTPReplyError","NNTPTemporaryError",
37  "NNTPPermanentError","NNTPProtocolError","NNTPDataError",
38  "error_reply","error_temp","error_perm","error_proto",
39  "error_data",]
40 
41 # Exceptions raised when an error or invalid response is received
42 class NNTPError(Exception):
43  """Base class for all nntplib exceptions"""
44  def __init__(self, *args):
45  apply(Exception.__init__, (self,)+args)
46  try:
47  self.response = args[0]
48  except IndexError:
49  self.response = 'No response given'
50 
51 class NNTPReplyError(NNTPError):
52  """Unexpected [123]xx reply"""
53  pass
54 
55 class NNTPTemporaryError(NNTPError):
56  """4xx errors"""
57  pass
58 
59 class NNTPPermanentError(NNTPError):
60  """5xx errors"""
61  pass
62 
63 class NNTPProtocolError(NNTPError):
64  """Response does not begin with [1-5]"""
65  pass
66 
67 class NNTPDataError(NNTPError):
68  """Error in response data"""
69  pass
70 
71 # for backwards compatibility
72 error_reply = NNTPReplyError
73 error_temp = NNTPTemporaryError
74 error_perm = NNTPPermanentError
75 error_proto = NNTPProtocolError
76 error_data = NNTPDataError
77 
78 
79 
80 # Standard port used by NNTP servers
81 NNTP_PORT = 119
82 
83 
84 # Response numbers that are followed by additional text (e.g. article)
85 LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
86 
87 
88 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
89 CRLF = '\r\n'
90 
91 
92 
93 # The class itself
94 class NNTP:
95  def __init__(self, host, port=NNTP_PORT, user=None, password=None,
96  readermode=None):
97  """Initialize an instance. Arguments:
98  - host: hostname to connect to
99  - port: port to connect to (default the standard NNTP port)
100  - user: username to authenticate with
101  - password: password to use with username
102  - readermode: if true, send 'mode reader' command after
103  connecting.
104 
105  readermode is sometimes necessary if you are connecting to an
106  NNTP server on the local machine and intend to call
107  reader-specific comamnds, such as `group'. If you get
108  unexpected NNTPPermanentErrors, you might need to set
109  readermode.
110  """
111  self.host = host
112  self.port = port
113  self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
114  self.sock.connect((self.host, self.port))
115  self.file = self.sock.makefile('rb')
116  self.debugging = 0
117  self.welcome = self.getresp()
118 
119  # 'mode reader' is sometimes necessary to enable 'reader' mode.
120  # However, the order in which 'mode reader' and 'authinfo' need to
121  # arrive differs between some NNTP servers. Try to send
122  # 'mode reader', and if it fails with an authorization failed
123  # error, try again after sending authinfo.
124  readermode_afterauth = 0
125  if readermode:
126  try:
127  self.welcome = self.shortcmd('mode reader')
128  except NNTPPermanentError:
129  # error 500, probably 'not implemented'
130  pass
131  except NNTPTemporaryError, e:
132  if user and e.response[:3] == '480':
133  # Need authorization before 'mode reader'
134  readermode_afterauth = 1
135  else:
136  raise
137  if user:
138  resp = self.shortcmd('authinfo user '+user)
139  if resp[:3] == '381':
140  if not password:
141  raise NNTPReplyError(resp)
142  else:
143  resp = self.shortcmd(
144  'authinfo pass '+password)
145  if resp[:3] != '281':
146  raise NNTPPermanentError(resp)
147  if readermode_afterauth:
148  try:
149  self.welcome = self.shortcmd('mode reader')
150  except NNTPPermanentError:
151  # error 500, probably 'not implemented'
152  pass
153 
154 
155  # Get the welcome message from the server
156  # (this is read and squirreled away by __init__()).
157  # If the response code is 200, posting is allowed;
158  # if it 201, posting is not allowed
159 
160  def getwelcome(self):
161  """Get the welcome message from the server
162  (this is read and squirreled away by __init__()).
163  If the response code is 200, posting is allowed;
164  if it 201, posting is not allowed."""
165 
166  if self.debugging: print '*welcome*', `self.welcome`
167  return self.welcome
168 
169  def set_debuglevel(self, level):
170  """Set the debugging level. Argument 'level' means:
171  0: no debugging output (default)
172  1: print commands and responses but not body text etc.
173  2: also print raw lines read and sent before stripping CR/LF"""
174 
175  self.debugging = level
176  debug = set_debuglevel
177 
178  def putline(self, line):
179  """Internal: send one line to the server, appending CRLF."""
180  line = line + CRLF
181  if self.debugging > 1: print '*put*', `line`
182  self.sock.sendall(line)
183 
184  def putcmd(self, line):
185  """Internal: send one command to the server (through putline())."""
186  if self.debugging: print '*cmd*', `line`
187  self.putline(line)
188 
189  def getline(self):
190  """Internal: return one line from the server, stripping CRLF.
191  Raise EOFError if the connection is closed."""
192  line = self.file.readline()
193  if self.debugging > 1:
194  print '*get*', `line`
195  if not line: raise EOFError
196  if line[-2:] == CRLF: line = line[:-2]
197  elif line[-1:] in CRLF: line = line[:-1]
198  return line
199 
200  def getresp(self):
201  """Internal: get a response from the server.
202  Raise various errors if the response indicates an error."""
203  resp = self.getline()
204  if self.debugging: print '*resp*', `resp`
205  c = resp[:1]
206  if c == '4':
207  raise NNTPTemporaryError(resp)
208  if c == '5':
209  raise NNTPPermanentError(resp)
210  if c not in '123':
211  raise NNTPProtocolError(resp)
212  return resp
213 
214  def getlongresp(self, file=None):
215  """Internal: get a response plus following text from the server.
216  Raise various errors if the response indicates an error."""
217 
218  openedFile = None
219  try:
220  # If a string was passed then open a file with that name
221  if isinstance(file, types.StringType):
222  openedFile = file = open(file, "w")
223 
224  resp = self.getresp()
225  if resp[:3] not in LONGRESP:
226  raise NNTPReplyError(resp)
227  list = []
228  while 1:
229  line = self.getline()
230  if line == '.':
231  break
232  if line[:2] == '..':
233  line = line[1:]
234  if file:
235  file.write(line + "\n")
236  else:
237  list.append(line)
238  finally:
239  # If this method created the file, then it must close it
240  if openedFile:
241  openedFile.close()
242 
243  return resp, list
244 
245  def shortcmd(self, line):
246  """Internal: send a command and get the response."""
247  self.putcmd(line)
248  return self.getresp()
249 
250  def longcmd(self, line, file=None):
251  """Internal: send a command and get the response plus following text."""
252  self.putcmd(line)
253  return self.getlongresp(file)
254 
255  def newgroups(self, date, time):
256  """Process a NEWGROUPS command. Arguments:
257  - date: string 'yymmdd' indicating the date
258  - time: string 'hhmmss' indicating the time
259  Return:
260  - resp: server response if successful
261  - list: list of newsgroup names"""
262 
263  return self.longcmd('NEWGROUPS ' + date + ' ' + time)
264 
265  def newnews(self, group, date, time):
266  """Process a NEWNEWS command. Arguments:
267  - group: group name or '*'
268  - date: string 'yymmdd' indicating the date
269  - time: string 'hhmmss' indicating the time
270  Return:
271  - resp: server response if successful
272  - list: list of article ids"""
273 
274  cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
275  return self.longcmd(cmd)
276 
277  def list(self):
278  """Process a LIST command. Return:
279  - resp: server response if successful
280  - list: list of (group, last, first, flag) (strings)"""
281 
282  resp, list = self.longcmd('LIST')
283  for i in range(len(list)):
284  # Parse lines into "group last first flag"
285  list[i] = tuple(list[i].split())
286  return resp, list
287 
288  def group(self, name):
289  """Process a GROUP command. Argument:
290  - group: the group name
291  Returns:
292  - resp: server response if successful
293  - count: number of articles (string)
294  - first: first article number (string)
295  - last: last article number (string)
296  - name: the group name"""
297 
298  resp = self.shortcmd('GROUP ' + name)
299  if resp[:3] != '211':
300  raise NNTPReplyError(resp)
301  words = resp.split()
302  count = first = last = 0
303  n = len(words)
304  if n > 1:
305  count = words[1]
306  if n > 2:
307  first = words[2]
308  if n > 3:
309  last = words[3]
310  if n > 4:
311  name = words[4].lower()
312  return resp, count, first, last, name
313 
314  def help(self):
315  """Process a HELP command. Returns:
316  - resp: server response if successful
317  - list: list of strings"""
318 
319  return self.longcmd('HELP')
320 
321  def statparse(self, resp):
322  """Internal: parse the response of a STAT, NEXT or LAST command."""
323  if resp[:2] != '22':
324  raise NNTPReplyError(resp)
325  words = resp.split()
326  nr = 0
327  id = ''
328  n = len(words)
329  if n > 1:
330  nr = words[1]
331  if n > 2:
332  id = words[2]
333  return resp, nr, id
334 
335  def statcmd(self, line):
336  """Internal: process a STAT, NEXT or LAST command."""
337  resp = self.shortcmd(line)
338  return self.statparse(resp)
339 
340  def stat(self, id):
341  """Process a STAT command. Argument:
342  - id: article number or message id
343  Returns:
344  - resp: server response if successful
345  - nr: the article number
346  - id: the article id"""
347 
348  return self.statcmd('STAT ' + id)
349 
350  def next(self):
351  """Process a NEXT command. No arguments. Return as for STAT."""
352  return self.statcmd('NEXT')
353 
354  def last(self):
355  """Process a LAST command. No arguments. Return as for STAT."""
356  return self.statcmd('LAST')
357 
358  def artcmd(self, line, file=None):
359  """Internal: process a HEAD, BODY or ARTICLE command."""
360  resp, list = self.longcmd(line, file)
361  resp, nr, id = self.statparse(resp)
362  return resp, nr, id, list
363 
364  def head(self, id):
365  """Process a HEAD command. Argument:
366  - id: article number or message id
367  Returns:
368  - resp: server response if successful
369  - nr: article number
370  - id: message id
371  - list: the lines of the article's header"""
372 
373  return self.artcmd('HEAD ' + id)
374 
375  def body(self, id, file=None):
376  """Process a BODY command. Argument:
377  - id: article number or message id
378  - file: Filename string or file object to store the article in
379  Returns:
380  - resp: server response if successful
381  - nr: article number
382  - id: message id
383  - list: the lines of the article's body or an empty list
384  if file was used"""
385 
386  return self.artcmd('BODY ' + id, file)
387 
388  def article(self, id):
389  """Process an ARTICLE command. Argument:
390  - id: article number or message id
391  Returns:
392  - resp: server response if successful
393  - nr: article number
394  - id: message id
395  - list: the lines of the article"""
396 
397  return self.artcmd('ARTICLE ' + id)
398 
399  def slave(self):
400  """Process a SLAVE command. Returns:
401  - resp: server response if successful"""
402 
403  return self.shortcmd('SLAVE')
404 
405  def xhdr(self, hdr, str):
406  """Process an XHDR command (optional server extension). Arguments:
407  - hdr: the header type (e.g. 'subject')
408  - str: an article nr, a message id, or a range nr1-nr2
409  Returns:
410  - resp: server response if successful
411  - list: list of (nr, value) strings"""
412 
413  pat = re.compile('^([0-9]+) ?(.*)\n?')
414  resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str)
415  for i in range(len(lines)):
416  line = lines[i]
417  m = pat.match(line)
418  if m:
419  lines[i] = m.group(1, 2)
420  return resp, lines
421 
422  def xover(self,start,end):
423  """Process an XOVER command (optional server extension) Arguments:
424  - start: start of range
425  - end: end of range
426  Returns:
427  - resp: server response if successful
428  - list: list of (art-nr, subject, poster, date,
429  id, references, size, lines)"""
430 
431  resp, lines = self.longcmd('XOVER ' + start + '-' + end)
432  xover_lines = []
433  for line in lines:
434  elem = line.split("\t")
435  try:
436  xover_lines.append((elem[0],
437  elem[1],
438  elem[2],
439  elem[3],
440  elem[4],
441  elem[5].split(),
442  elem[6],
443  elem[7]))
444  except IndexError:
445  raise NNTPDataError(line)
446  return resp,xover_lines
447 
448  def xgtitle(self, group):
449  """Process an XGTITLE command (optional server extension) Arguments:
450  - group: group name wildcard (i.e. news.*)
451  Returns:
452  - resp: server response if successful
453  - list: list of (name,title) strings"""
454 
455  line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
456  resp, raw_lines = self.longcmd('XGTITLE ' + group)
457  lines = []
458  for raw_line in raw_lines:
459  match = line_pat.search(raw_line.strip())
460  if match:
461  lines.append(match.group(1, 2))
462  return resp, lines
463 
464  def xpath(self,id):
465  """Process an XPATH command (optional server extension) Arguments:
466  - id: Message id of article
467  Returns:
468  resp: server response if successful
469  path: directory path to article"""
470 
471  resp = self.shortcmd("XPATH " + id)
472  if resp[:3] != '223':
473  raise NNTPReplyError(resp)
474  try:
475  [resp_num, path] = resp.split()
476  except ValueError:
477  raise NNTPReplyError(resp)
478  else:
479  return resp, path
480 
481  def date (self):
482  """Process the DATE command. Arguments:
483  None
484  Returns:
485  resp: server response if successful
486  date: Date suitable for newnews/newgroups commands etc.
487  time: Time suitable for newnews/newgroups commands etc."""
488 
489  resp = self.shortcmd("DATE")
490  if resp[:3] != '111':
491  raise NNTPReplyError(resp)
492  elem = resp.split()
493  if len(elem) != 2:
494  raise NNTPDataError(resp)
495  date = elem[1][2:8]
496  time = elem[1][-6:]
497  if len(date) != 6 or len(time) != 6:
498  raise NNTPDataError(resp)
499  return resp, date, time
500 
501 
502  def post(self, f):
503  """Process a POST command. Arguments:
504  - f: file containing the article
505  Returns:
506  - resp: server response if successful"""
507 
508  resp = self.shortcmd('POST')
509  # Raises error_??? if posting is not allowed
510  if resp[0] != '3':
511  raise NNTPReplyError(resp)
512  while 1:
513  line = f.readline()
514  if not line:
515  break
516  if line[-1] == '\n':
517  line = line[:-1]
518  if line[:1] == '.':
519  line = '.' + line
520  self.putline(line)
521  self.putline('.')
522  return self.getresp()
523 
524  def ihave(self, id, f):
525  """Process an IHAVE command. Arguments:
526  - id: message-id of the article
527  - f: file containing the article
528  Returns:
529  - resp: server response if successful
530  Note that if the server refuses the article an exception is raised."""
531 
532  resp = self.shortcmd('IHAVE ' + id)
533  # Raises error_??? if the server already has it
534  if resp[0] != '3':
535  raise NNTPReplyError(resp)
536  while 1:
537  line = f.readline()
538  if not line:
539  break
540  if line[-1] == '\n':
541  line = line[:-1]
542  if line[:1] == '.':
543  line = '.' + line
544  self.putline(line)
545  self.putline('.')
546  return self.getresp()
547 
548  def quit(self):
549  """Process a QUIT command and close the socket. Returns:
550  - resp: server response if successful"""
551 
552  resp = self.shortcmd('QUIT')
553  self.file.close()
554  self.sock.close()
555  del self.file, self.sock
556  return resp
557 
558 
559 def _test():
560  """Minimal test function."""
561  s = NNTP('news', readermode='reader')
562  resp, count, first, last, name = s.group('comp.lang.python')
563  print resp
564  print 'Group', name, 'has', count, 'articles, range', first, 'to', last
565  resp, subs = s.xhdr('subject', first + '-' + last)
566  print resp
567  for item in subs:
568  print "%7s %s" % item
569  resp = s.quit()
570  print resp
571 
572 
573 # Run the test when run as a script
574 if __name__ == '__main__':
575  _test()