python-2.5.2/win32/Lib/nntplib.py
changeset 0 ae805ac0140d
equal deleted inserted replaced
-1:000000000000 0:ae805ac0140d
       
     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 
       
    35 __all__ = ["NNTP","NNTPReplyError","NNTPTemporaryError",
       
    36            "NNTPPermanentError","NNTPProtocolError","NNTPDataError",
       
    37            "error_reply","error_temp","error_perm","error_proto",
       
    38            "error_data",]
       
    39 
       
    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)
       
    45         try:
       
    46             self.response = args[0]
       
    47         except IndexError:
       
    48             self.response = 'No response given'
       
    49 
       
    50 class NNTPReplyError(NNTPError):
       
    51     """Unexpected [123]xx reply"""
       
    52     pass
       
    53 
       
    54 class NNTPTemporaryError(NNTPError):
       
    55     """4xx errors"""
       
    56     pass
       
    57 
       
    58 class NNTPPermanentError(NNTPError):
       
    59     """5xx errors"""
       
    60     pass
       
    61 
       
    62 class NNTPProtocolError(NNTPError):
       
    63     """Response does not begin with [1-5]"""
       
    64     pass
       
    65 
       
    66 class NNTPDataError(NNTPError):
       
    67     """Error in response data"""
       
    68     pass
       
    69 
       
    70 # for backwards compatibility
       
    71 error_reply = NNTPReplyError
       
    72 error_temp = NNTPTemporaryError
       
    73 error_perm = NNTPPermanentError
       
    74 error_proto = NNTPProtocolError
       
    75 error_data = NNTPDataError
       
    76 
       
    77 
       
    78 
       
    79 # Standard port used by NNTP servers
       
    80 NNTP_PORT = 119
       
    81 
       
    82 
       
    83 # Response numbers that are followed by additional text (e.g. article)
       
    84 LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
       
    85 
       
    86 
       
    87 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
       
    88 CRLF = '\r\n'
       
    89 
       
    90 
       
    91 
       
    92 # The class itself
       
    93 class NNTP:
       
    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
       
   102                       connecting.
       
   103 
       
   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
       
   108         readermode.
       
   109         """
       
   110         self.host = host
       
   111         self.port = port
       
   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')
       
   115         self.debugging = 0
       
   116         self.welcome = self.getresp()
       
   117 
       
   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
       
   124         if readermode:
       
   125             try:
       
   126                 self.welcome = self.shortcmd('mode reader')
       
   127             except NNTPPermanentError:
       
   128                 # error 500, probably 'not implemented'
       
   129                 pass
       
   130             except NNTPTemporaryError, e:
       
   131                 if user and e.response[:3] == '480':
       
   132                     # Need authorization before 'mode reader'
       
   133                     readermode_afterauth = 1
       
   134                 else:
       
   135                     raise
       
   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.
       
   138         try:
       
   139             if usenetrc and not user:
       
   140                 import netrc
       
   141                 credentials = netrc.netrc()
       
   142                 auth = credentials.authenticators(host)
       
   143                 if auth:
       
   144                     user = auth[0]
       
   145                     password = auth[2]
       
   146         except IOError:
       
   147             pass
       
   148         # Perform NNRP authentication if needed.
       
   149         if user:
       
   150             resp = self.shortcmd('authinfo user '+user)
       
   151             if resp[:3] == '381':
       
   152                 if not password:
       
   153                     raise NNTPReplyError(resp)
       
   154                 else:
       
   155                     resp = self.shortcmd(
       
   156                             'authinfo pass '+password)
       
   157                     if resp[:3] != '281':
       
   158                         raise NNTPPermanentError(resp)
       
   159             if readermode_afterauth:
       
   160                 try:
       
   161                     self.welcome = self.shortcmd('mode reader')
       
   162                 except NNTPPermanentError:
       
   163                     # error 500, probably 'not implemented'
       
   164                     pass
       
   165 
       
   166 
       
   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
       
   171 
       
   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."""
       
   177 
       
   178         if self.debugging: print '*welcome*', repr(self.welcome)
       
   179         return self.welcome
       
   180 
       
   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"""
       
   186 
       
   187         self.debugging = level
       
   188     debug = set_debuglevel
       
   189 
       
   190     def putline(self, line):
       
   191         """Internal: send one line to the server, appending CRLF."""
       
   192         line = line + CRLF
       
   193         if self.debugging > 1: print '*put*', repr(line)
       
   194         self.sock.sendall(line)
       
   195 
       
   196     def putcmd(self, line):
       
   197         """Internal: send one command to the server (through putline())."""
       
   198         if self.debugging: print '*cmd*', repr(line)
       
   199         self.putline(line)
       
   200 
       
   201     def getline(self):
       
   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]
       
   210         return line
       
   211 
       
   212     def getresp(self):
       
   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)
       
   217         c = resp[:1]
       
   218         if c == '4':
       
   219             raise NNTPTemporaryError(resp)
       
   220         if c == '5':
       
   221             raise NNTPPermanentError(resp)
       
   222         if c not in '123':
       
   223             raise NNTPProtocolError(resp)
       
   224         return resp
       
   225 
       
   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."""
       
   229 
       
   230         openedFile = None
       
   231         try:
       
   232             # If a string was passed then open a file with that name
       
   233             if isinstance(file, str):
       
   234                 openedFile = file = open(file, "w")
       
   235 
       
   236             resp = self.getresp()
       
   237             if resp[:3] not in LONGRESP:
       
   238                 raise NNTPReplyError(resp)
       
   239             list = []
       
   240             while 1:
       
   241                 line = self.getline()
       
   242                 if line == '.':
       
   243                     break
       
   244                 if line[:2] == '..':
       
   245                     line = line[1:]
       
   246                 if file:
       
   247                     file.write(line + "\n")
       
   248                 else:
       
   249                     list.append(line)
       
   250         finally:
       
   251             # If this method created the file, then it must close it
       
   252             if openedFile:
       
   253                 openedFile.close()
       
   254 
       
   255         return resp, list
       
   256 
       
   257     def shortcmd(self, line):
       
   258         """Internal: send a command and get the response."""
       
   259         self.putcmd(line)
       
   260         return self.getresp()
       
   261 
       
   262     def longcmd(self, line, file=None):
       
   263         """Internal: send a command and get the response plus following text."""
       
   264         self.putcmd(line)
       
   265         return self.getlongresp(file)
       
   266 
       
   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
       
   271         Return:
       
   272         - resp: server response if successful
       
   273         - list: list of newsgroup names"""
       
   274 
       
   275         return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
       
   276 
       
   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
       
   282         Return:
       
   283         - resp: server response if successful
       
   284         - list: list of message ids"""
       
   285 
       
   286         cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
       
   287         return self.longcmd(cmd, file)
       
   288 
       
   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)"""
       
   293 
       
   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())
       
   298         return resp, list
       
   299 
       
   300     def description(self, group):
       
   301 
       
   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.
       
   305 
       
   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.
       
   309 
       
   310         NOTE: This neither checks for a wildcard in 'group' nor does
       
   311         it check whether the group actually exists."""
       
   312 
       
   313         resp, lines = self.descriptions(group)
       
   314         if len(lines) == 0:
       
   315             return ""
       
   316         else:
       
   317             return lines[0][1]
       
   318 
       
   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
       
   327             # NEWSGROUPS.
       
   328             resp, raw_lines = self.longcmd('XGTITLE ' + group_pattern)
       
   329         lines = []
       
   330         for raw_line in raw_lines:
       
   331             match = line_pat.search(raw_line.strip())
       
   332             if match:
       
   333                 lines.append(match.group(1, 2))
       
   334         return resp, lines
       
   335 
       
   336     def group(self, name):
       
   337         """Process a GROUP command.  Argument:
       
   338         - group: the group name
       
   339         Returns:
       
   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"""
       
   345 
       
   346         resp = self.shortcmd('GROUP ' + name)
       
   347         if resp[:3] != '211':
       
   348             raise NNTPReplyError(resp)
       
   349         words = resp.split()
       
   350         count = first = last = 0
       
   351         n = len(words)
       
   352         if n > 1:
       
   353             count = words[1]
       
   354             if n > 2:
       
   355                 first = words[2]
       
   356                 if n > 3:
       
   357                     last = words[3]
       
   358                     if n > 4:
       
   359                         name = words[4].lower()
       
   360         return resp, count, first, last, name
       
   361 
       
   362     def help(self, file=None):
       
   363         """Process a HELP command.  Returns:
       
   364         - resp: server response if successful
       
   365         - list: list of strings"""
       
   366 
       
   367         return self.longcmd('HELP',file)
       
   368 
       
   369     def statparse(self, resp):
       
   370         """Internal: parse the response of a STAT, NEXT or LAST command."""
       
   371         if resp[:2] != '22':
       
   372             raise NNTPReplyError(resp)
       
   373         words = resp.split()
       
   374         nr = 0
       
   375         id = ''
       
   376         n = len(words)
       
   377         if n > 1:
       
   378             nr = words[1]
       
   379             if n > 2:
       
   380                 id = words[2]
       
   381         return resp, nr, id
       
   382 
       
   383     def statcmd(self, line):
       
   384         """Internal: process a STAT, NEXT or LAST command."""
       
   385         resp = self.shortcmd(line)
       
   386         return self.statparse(resp)
       
   387 
       
   388     def stat(self, id):
       
   389         """Process a STAT command.  Argument:
       
   390         - id: article number or message id
       
   391         Returns:
       
   392         - resp: server response if successful
       
   393         - nr:   the article number
       
   394         - id:   the message id"""
       
   395 
       
   396         return self.statcmd('STAT ' + id)
       
   397 
       
   398     def next(self):
       
   399         """Process a NEXT command.  No arguments.  Return as for STAT."""
       
   400         return self.statcmd('NEXT')
       
   401 
       
   402     def last(self):
       
   403         """Process a LAST command.  No arguments.  Return as for STAT."""
       
   404         return self.statcmd('LAST')
       
   405 
       
   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
       
   411 
       
   412     def head(self, id):
       
   413         """Process a HEAD command.  Argument:
       
   414         - id: article number or message id
       
   415         Returns:
       
   416         - resp: server response if successful
       
   417         - nr: article number
       
   418         - id: message id
       
   419         - list: the lines of the article's header"""
       
   420 
       
   421         return self.artcmd('HEAD ' + id)
       
   422 
       
   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
       
   427         Returns:
       
   428         - resp: server response if successful
       
   429         - nr: article number
       
   430         - id: message id
       
   431         - list: the lines of the article's body or an empty list
       
   432                 if file was used"""
       
   433 
       
   434         return self.artcmd('BODY ' + id, file)
       
   435 
       
   436     def article(self, id):
       
   437         """Process an ARTICLE command.  Argument:
       
   438         - id: article number or message id
       
   439         Returns:
       
   440         - resp: server response if successful
       
   441         - nr: article number
       
   442         - id: message id
       
   443         - list: the lines of the article"""
       
   444 
       
   445         return self.artcmd('ARTICLE ' + id)
       
   446 
       
   447     def slave(self):
       
   448         """Process a SLAVE command.  Returns:
       
   449         - resp: server response if successful"""
       
   450 
       
   451         return self.shortcmd('SLAVE')
       
   452 
       
   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
       
   457         Returns:
       
   458         - resp: server response if successful
       
   459         - list: list of (nr, value) strings"""
       
   460 
       
   461         pat = re.compile('^([0-9]+) ?(.*)\n?')
       
   462         resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str, file)
       
   463         for i in range(len(lines)):
       
   464             line = lines[i]
       
   465             m = pat.match(line)
       
   466             if m:
       
   467                 lines[i] = m.group(1, 2)
       
   468         return resp, lines
       
   469 
       
   470     def xover(self, start, end, file=None):
       
   471         """Process an XOVER command (optional server extension) Arguments:
       
   472         - start: start of range
       
   473         - end: end of range
       
   474         Returns:
       
   475         - resp: server response if successful
       
   476         - list: list of (art-nr, subject, poster, date,
       
   477                          id, references, size, lines)"""
       
   478 
       
   479         resp, lines = self.longcmd('XOVER ' + start + '-' + end, file)
       
   480         xover_lines = []
       
   481         for line in lines:
       
   482             elem = line.split("\t")
       
   483             try:
       
   484                 xover_lines.append((elem[0],
       
   485                                     elem[1],
       
   486                                     elem[2],
       
   487                                     elem[3],
       
   488                                     elem[4],
       
   489                                     elem[5].split(),
       
   490                                     elem[6],
       
   491                                     elem[7]))
       
   492             except IndexError:
       
   493                 raise NNTPDataError(line)
       
   494         return resp,xover_lines
       
   495 
       
   496     def xgtitle(self, group, file=None):
       
   497         """Process an XGTITLE command (optional server extension) Arguments:
       
   498         - group: group name wildcard (i.e. news.*)
       
   499         Returns:
       
   500         - resp: server response if successful
       
   501         - list: list of (name,title) strings"""
       
   502 
       
   503         line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
       
   504         resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
       
   505         lines = []
       
   506         for raw_line in raw_lines:
       
   507             match = line_pat.search(raw_line.strip())
       
   508             if match:
       
   509                 lines.append(match.group(1, 2))
       
   510         return resp, lines
       
   511 
       
   512     def xpath(self,id):
       
   513         """Process an XPATH command (optional server extension) Arguments:
       
   514         - id: Message id of article
       
   515         Returns:
       
   516         resp: server response if successful
       
   517         path: directory path to article"""
       
   518 
       
   519         resp = self.shortcmd("XPATH " + id)
       
   520         if resp[:3] != '223':
       
   521             raise NNTPReplyError(resp)
       
   522         try:
       
   523             [resp_num, path] = resp.split()
       
   524         except ValueError:
       
   525             raise NNTPReplyError(resp)
       
   526         else:
       
   527             return resp, path
       
   528 
       
   529     def date (self):
       
   530         """Process the DATE command. Arguments:
       
   531         None
       
   532         Returns:
       
   533         resp: server response if successful
       
   534         date: Date suitable for newnews/newgroups commands etc.
       
   535         time: Time suitable for newnews/newgroups commands etc."""
       
   536 
       
   537         resp = self.shortcmd("DATE")
       
   538         if resp[:3] != '111':
       
   539             raise NNTPReplyError(resp)
       
   540         elem = resp.split()
       
   541         if len(elem) != 2:
       
   542             raise NNTPDataError(resp)
       
   543         date = elem[1][2:8]
       
   544         time = elem[1][-6:]
       
   545         if len(date) != 6 or len(time) != 6:
       
   546             raise NNTPDataError(resp)
       
   547         return resp, date, time
       
   548 
       
   549 
       
   550     def post(self, f):
       
   551         """Process a POST command.  Arguments:
       
   552         - f: file containing the article
       
   553         Returns:
       
   554         - resp: server response if successful"""
       
   555 
       
   556         resp = self.shortcmd('POST')
       
   557         # Raises error_??? if posting is not allowed
       
   558         if resp[0] != '3':
       
   559             raise NNTPReplyError(resp)
       
   560         while 1:
       
   561             line = f.readline()
       
   562             if not line:
       
   563                 break
       
   564             if line[-1] == '\n':
       
   565                 line = line[:-1]
       
   566             if line[:1] == '.':
       
   567                 line = '.' + line
       
   568             self.putline(line)
       
   569         self.putline('.')
       
   570         return self.getresp()
       
   571 
       
   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
       
   576         Returns:
       
   577         - resp: server response if successful
       
   578         Note that if the server refuses the article an exception is raised."""
       
   579 
       
   580         resp = self.shortcmd('IHAVE ' + id)
       
   581         # Raises error_??? if the server already has it
       
   582         if resp[0] != '3':
       
   583             raise NNTPReplyError(resp)
       
   584         while 1:
       
   585             line = f.readline()
       
   586             if not line:
       
   587                 break
       
   588             if line[-1] == '\n':
       
   589                 line = line[:-1]
       
   590             if line[:1] == '.':
       
   591                 line = '.' + line
       
   592             self.putline(line)
       
   593         self.putline('.')
       
   594         return self.getresp()
       
   595 
       
   596     def quit(self):
       
   597         """Process a QUIT command and close the socket.  Returns:
       
   598         - resp: server response if successful"""
       
   599 
       
   600         resp = self.shortcmd('QUIT')
       
   601         self.file.close()
       
   602         self.sock.close()
       
   603         del self.file, self.sock
       
   604         return resp
       
   605 
       
   606 
       
   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__':
       
   613     import os
       
   614     newshost = 'news' and os.environ["NNTPSERVER"]
       
   615     if newshost.find('.') == -1:
       
   616         mode = 'readermode'
       
   617     else:
       
   618         mode = None
       
   619     s = NNTP(newshost, readermode=mode)
       
   620     resp, count, first, last, name = s.group('comp.lang.python')
       
   621     print resp
       
   622     print 'Group', name, 'has', count, 'articles, range', first, 'to', last
       
   623     resp, subs = s.xhdr('subject', first + '-' + last)
       
   624     print resp
       
   625     for item in subs:
       
   626         print "%7s %s" % item
       
   627     resp = s.quit()
       
   628     print resp