PyXR

c:\python24\lib \ nntplib.py



0001 """An NNTP client class based on RFC 977: Network News Transfer Protocol.
0002 
0003 Example:
0004 
0005 >>> from nntplib import NNTP
0006 >>> s = NNTP('news')
0007 >>> resp, count, first, last, name = s.group('comp.lang.python')
0008 >>> print 'Group', name, 'has', count, 'articles, range', first, 'to', last
0009 Group comp.lang.python has 51 articles, range 5770 to 5821
0010 >>> resp, subs = s.xhdr('subject', first + '-' + last)
0011 >>> resp = s.quit()
0012 >>>
0013 
0014 Here 'resp' is the server response line.
0015 Error responses are turned into exceptions.
0016 
0017 To post an article from a file:
0018 >>> f = open(filename, 'r') # file containing article, including header
0019 >>> resp = s.post(f)
0020 >>>
0021 
0022 For descriptions of all methods, read the comments in the code below.
0023 Note that all arguments and return values representing article numbers
0024 are strings, not numbers, since they are rarely used for calculations.
0025 """
0026 
0027 # RFC 977 by Brian Kantor and Phil Lapsley.
0028 # xover, xgtitle, xpath, date methods by Kevan Heydon
0029 
0030 
0031 # Imports
0032 import re
0033 import socket
0034 
0035 __all__ = ["NNTP","NNTPReplyError","NNTPTemporaryError",
0036            "NNTPPermanentError","NNTPProtocolError","NNTPDataError",
0037            "error_reply","error_temp","error_perm","error_proto",
0038            "error_data",]
0039 
0040 # Exceptions raised when an error or invalid response is received
0041 class NNTPError(Exception):
0042     """Base class for all nntplib exceptions"""
0043     def __init__(self, *args):
0044         Exception.__init__(self, *args)
0045         try:
0046             self.response = args[0]
0047         except IndexError:
0048             self.response = 'No response given'
0049 
0050 class NNTPReplyError(NNTPError):
0051     """Unexpected [123]xx reply"""
0052     pass
0053 
0054 class NNTPTemporaryError(NNTPError):
0055     """4xx errors"""
0056     pass
0057 
0058 class NNTPPermanentError(NNTPError):
0059     """5xx errors"""
0060     pass
0061 
0062 class NNTPProtocolError(NNTPError):
0063     """Response does not begin with [1-5]"""
0064     pass
0065 
0066 class NNTPDataError(NNTPError):
0067     """Error in response data"""
0068     pass
0069 
0070 # for backwards compatibility
0071 error_reply = NNTPReplyError
0072 error_temp = NNTPTemporaryError
0073 error_perm = NNTPPermanentError
0074 error_proto = NNTPProtocolError
0075 error_data = NNTPDataError
0076 
0077 
0078 
0079 # Standard port used by NNTP servers
0080 NNTP_PORT = 119
0081 
0082 
0083 # Response numbers that are followed by additional text (e.g. article)
0084 LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
0085 
0086 
0087 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
0088 CRLF = '\r\n'
0089 
0090 
0091 
0092 # The class itself
0093 class NNTP:
0094     def __init__(self, host, port=NNTP_PORT, user=None, password=None,
0095                  readermode=None, usenetrc=True):
0096         """Initialize an instance.  Arguments:
0097         - host: hostname to connect to
0098         - port: port to connect to (default the standard NNTP port)
0099         - user: username to authenticate with
0100         - password: password to use with username
0101         - readermode: if true, send 'mode reader' command after
0102                       connecting.
0103 
0104         readermode is sometimes necessary if you are connecting to an
0105         NNTP server on the local machine and intend to call
0106         reader-specific comamnds, such as `group'.  If you get
0107         unexpected NNTPPermanentErrors, you might need to set
0108         readermode.
0109         """
0110         self.host = host
0111         self.port = port
0112         self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
0113         self.sock.connect((self.host, self.port))
0114         self.file = self.sock.makefile('rb')
0115         self.debugging = 0
0116         self.welcome = self.getresp()
0117 
0118         # 'mode reader' is sometimes necessary to enable 'reader' mode.
0119         # However, the order in which 'mode reader' and 'authinfo' need to
0120         # arrive differs between some NNTP servers. Try to send
0121         # 'mode reader', and if it fails with an authorization failed
0122         # error, try again after sending authinfo.
0123         readermode_afterauth = 0
0124         if readermode:
0125             try:
0126                 self.welcome = self.shortcmd('mode reader')
0127             except NNTPPermanentError:
0128                 # error 500, probably 'not implemented'
0129                 pass
0130             except NNTPTemporaryError, e:
0131                 if user and e.response[:3] == '480':
0132                     # Need authorization before 'mode reader'
0133                     readermode_afterauth = 1
0134                 else:
0135                     raise
0136         # If no login/password was specified, try to get them from ~/.netrc
0137         # Presume that if .netc has an entry, NNRP authentication is required.
0138         try:
0139             if usenetrc and not user:
0140                 import netrc
0141                 credentials = netrc.netrc()
0142                 auth = credentials.authenticators(host)
0143                 if auth:
0144                     user = auth[0]
0145                     password = auth[2]
0146         except IOError:
0147             pass
0148         # Perform NNRP authentication if needed.
0149         if user:
0150             resp = self.shortcmd('authinfo user '+user)
0151             if resp[:3] == '381':
0152                 if not password:
0153                     raise NNTPReplyError(resp)
0154                 else:
0155                     resp = self.shortcmd(
0156                             'authinfo pass '+password)
0157                     if resp[:3] != '281':
0158                         raise NNTPPermanentError(resp)
0159             if readermode_afterauth:
0160                 try:
0161                     self.welcome = self.shortcmd('mode reader')
0162                 except NNTPPermanentError:
0163                     # error 500, probably 'not implemented'
0164                     pass
0165 
0166 
0167     # Get the welcome message from the server
0168     # (this is read and squirreled away by __init__()).
0169     # If the response code is 200, posting is allowed;
0170     # if it 201, posting is not allowed
0171 
0172     def getwelcome(self):
0173         """Get the welcome message from the server
0174         (this is read and squirreled away by __init__()).
0175         If the response code is 200, posting is allowed;
0176         if it 201, posting is not allowed."""
0177 
0178         if self.debugging: print '*welcome*', repr(self.welcome)
0179         return self.welcome
0180 
0181     def set_debuglevel(self, level):
0182         """Set the debugging level.  Argument 'level' means:
0183         0: no debugging output (default)
0184         1: print commands and responses but not body text etc.
0185         2: also print raw lines read and sent before stripping CR/LF"""
0186 
0187         self.debugging = level
0188     debug = set_debuglevel
0189 
0190     def putline(self, line):
0191         """Internal: send one line to the server, appending CRLF."""
0192         line = line + CRLF
0193         if self.debugging > 1: print '*put*', repr(line)
0194         self.sock.sendall(line)
0195 
0196     def putcmd(self, line):
0197         """Internal: send one command to the server (through putline())."""
0198         if self.debugging: print '*cmd*', repr(line)
0199         self.putline(line)
0200 
0201     def getline(self):
0202         """Internal: return one line from the server, stripping CRLF.
0203         Raise EOFError if the connection is closed."""
0204         line = self.file.readline()
0205         if self.debugging > 1:
0206             print '*get*', repr(line)
0207         if not line: raise EOFError
0208         if line[-2:] == CRLF: line = line[:-2]
0209         elif line[-1:] in CRLF: line = line[:-1]
0210         return line
0211 
0212     def getresp(self):
0213         """Internal: get a response from the server.
0214         Raise various errors if the response indicates an error."""
0215         resp = self.getline()
0216         if self.debugging: print '*resp*', repr(resp)
0217         c = resp[:1]
0218         if c == '4':
0219             raise NNTPTemporaryError(resp)
0220         if c == '5':
0221             raise NNTPPermanentError(resp)
0222         if c not in '123':
0223             raise NNTPProtocolError(resp)
0224         return resp
0225 
0226     def getlongresp(self, file=None):
0227         """Internal: get a response plus following text from the server.
0228         Raise various errors if the response indicates an error."""
0229 
0230         openedFile = None
0231         try:
0232             # If a string was passed then open a file with that name
0233             if isinstance(file, str):
0234                 openedFile = file = open(file, "w")
0235 
0236             resp = self.getresp()
0237             if resp[:3] not in LONGRESP:
0238                 raise NNTPReplyError(resp)
0239             list = []
0240             while 1:
0241                 line = self.getline()
0242                 if line == '.':
0243                     break
0244                 if line[:2] == '..':
0245                     line = line[1:]
0246                 if file:
0247                     file.write(line + "\n")
0248                 else:
0249                     list.append(line)
0250         finally:
0251             # If this method created the file, then it must close it
0252             if openedFile:
0253                 openedFile.close()
0254 
0255         return resp, list
0256 
0257     def shortcmd(self, line):
0258         """Internal: send a command and get the response."""
0259         self.putcmd(line)
0260         return self.getresp()
0261 
0262     def longcmd(self, line, file=None):
0263         """Internal: send a command and get the response plus following text."""
0264         self.putcmd(line)
0265         return self.getlongresp(file)
0266 
0267     def newgroups(self, date, time, file=None):
0268         """Process a NEWGROUPS command.  Arguments:
0269         - date: string 'yymmdd' indicating the date
0270         - time: string 'hhmmss' indicating the time
0271         Return:
0272         - resp: server response if successful
0273         - list: list of newsgroup names"""
0274 
0275         return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
0276 
0277     def newnews(self, group, date, time, file=None):
0278         """Process a NEWNEWS command.  Arguments:
0279         - group: group name or '*'
0280         - date: string 'yymmdd' indicating the date
0281         - time: string 'hhmmss' indicating the time
0282         Return:
0283         - resp: server response if successful
0284         - list: list of article ids"""
0285 
0286         cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
0287         return self.longcmd(cmd, file)
0288 
0289     def list(self, file=None):
0290         """Process a LIST command.  Return:
0291         - resp: server response if successful
0292         - list: list of (group, last, first, flag) (strings)"""
0293 
0294         resp, list = self.longcmd('LIST', file)
0295         for i in range(len(list)):
0296             # Parse lines into "group last first flag"
0297             list[i] = tuple(list[i].split())
0298         return resp, list
0299 
0300     def description(self, group):
0301 
0302         """Get a description for a single group.  If more than one
0303         group matches ('group' is a pattern), return the first.  If no
0304         group matches, return an empty string.
0305 
0306         This elides the response code from the server, since it can
0307         only be '215' or '285' (for xgtitle) anyway.  If the response
0308         code is needed, use the 'descriptions' method.
0309 
0310         NOTE: This neither checks for a wildcard in 'group' nor does
0311         it check whether the group actually exists."""
0312 
0313         resp, lines = self.descriptions(group)
0314         if len(lines) == 0:
0315             return ""
0316         else:
0317             return lines[0][1]
0318 
0319     def descriptions(self, group_pattern):
0320         """Get descriptions for a range of groups."""
0321         line_pat = re.compile("^(?P<group>[^ \t]+)[ \t]+(.*)$")
0322         # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
0323         resp, raw_lines = self.longcmd('LIST NEWSGROUPS ' + group_pattern)
0324         if resp[:3] != "215":
0325             # Now the deprecated XGTITLE.  This either raises an error
0326             # or succeeds with the same output structure as LIST
0327             # NEWSGROUPS.
0328             resp, raw_lines = self.longcmd('XGTITLE ' + group_pattern)
0329         lines = []
0330         for raw_line in raw_lines:
0331             match = line_pat.search(raw_line.strip())
0332             if match:
0333                 lines.append(match.group(1, 2))
0334         return resp, lines
0335 
0336     def group(self, name):
0337         """Process a GROUP command.  Argument:
0338         - group: the group name
0339         Returns:
0340         - resp: server response if successful
0341         - count: number of articles (string)
0342         - first: first article number (string)
0343         - last: last article number (string)
0344         - name: the group name"""
0345 
0346         resp = self.shortcmd('GROUP ' + name)
0347         if resp[:3] != '211':
0348             raise NNTPReplyError(resp)
0349         words = resp.split()
0350         count = first = last = 0
0351         n = len(words)
0352         if n > 1:
0353             count = words[1]
0354             if n > 2:
0355                 first = words[2]
0356                 if n > 3:
0357                     last = words[3]
0358                     if n > 4:
0359                         name = words[4].lower()
0360         return resp, count, first, last, name
0361 
0362     def help(self, file=None):
0363         """Process a HELP command.  Returns:
0364         - resp: server response if successful
0365         - list: list of strings"""
0366 
0367         return self.longcmd('HELP',file)
0368 
0369     def statparse(self, resp):
0370         """Internal: parse the response of a STAT, NEXT or LAST command."""
0371         if resp[:2] != '22':
0372             raise NNTPReplyError(resp)
0373         words = resp.split()
0374         nr = 0
0375         id = ''
0376         n = len(words)
0377         if n > 1:
0378             nr = words[1]
0379             if n > 2:
0380                 id = words[2]
0381         return resp, nr, id
0382 
0383     def statcmd(self, line):
0384         """Internal: process a STAT, NEXT or LAST command."""
0385         resp = self.shortcmd(line)
0386         return self.statparse(resp)
0387 
0388     def stat(self, id):
0389         """Process a STAT command.  Argument:
0390         - id: article number or message id
0391         Returns:
0392         - resp: server response if successful
0393         - nr:   the article number
0394         - id:   the article id"""
0395 
0396         return self.statcmd('STAT ' + id)
0397 
0398     def next(self):
0399         """Process a NEXT command.  No arguments.  Return as for STAT."""
0400         return self.statcmd('NEXT')
0401 
0402     def last(self):
0403         """Process a LAST command.  No arguments.  Return as for STAT."""
0404         return self.statcmd('LAST')
0405 
0406     def artcmd(self, line, file=None):
0407         """Internal: process a HEAD, BODY or ARTICLE command."""
0408         resp, list = self.longcmd(line, file)
0409         resp, nr, id = self.statparse(resp)
0410         return resp, nr, id, list
0411 
0412     def head(self, id):
0413         """Process a HEAD command.  Argument:
0414         - id: article number or message id
0415         Returns:
0416         - resp: server response if successful
0417         - nr: article number
0418         - id: message id
0419         - list: the lines of the article's header"""
0420 
0421         return self.artcmd('HEAD ' + id)
0422 
0423     def body(self, id, file=None):
0424         """Process a BODY command.  Argument:
0425         - id: article number or message id
0426         - file: Filename string or file object to store the article in
0427         Returns:
0428         - resp: server response if successful
0429         - nr: article number
0430         - id: message id
0431         - list: the lines of the article's body or an empty list
0432                 if file was used"""
0433 
0434         return self.artcmd('BODY ' + id, file)
0435 
0436     def article(self, id):
0437         """Process an ARTICLE command.  Argument:
0438         - id: article number or message id
0439         Returns:
0440         - resp: server response if successful
0441         - nr: article number
0442         - id: message id
0443         - list: the lines of the article"""
0444 
0445         return self.artcmd('ARTICLE ' + id)
0446 
0447     def slave(self):
0448         """Process a SLAVE command.  Returns:
0449         - resp: server response if successful"""
0450 
0451         return self.shortcmd('SLAVE')
0452 
0453     def xhdr(self, hdr, str, file=None):
0454         """Process an XHDR command (optional server extension).  Arguments:
0455         - hdr: the header type (e.g. 'subject')
0456         - str: an article nr, a message id, or a range nr1-nr2
0457         Returns:
0458         - resp: server response if successful
0459         - list: list of (nr, value) strings"""
0460 
0461         pat = re.compile('^([0-9]+) ?(.*)\n?')
0462         resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str, file)
0463         for i in range(len(lines)):
0464             line = lines[i]
0465             m = pat.match(line)
0466             if m:
0467                 lines[i] = m.group(1, 2)
0468         return resp, lines
0469 
0470     def xover(self, start, end, file=None):
0471         """Process an XOVER command (optional server extension) Arguments:
0472         - start: start of range
0473         - end: end of range
0474         Returns:
0475         - resp: server response if successful
0476         - list: list of (art-nr, subject, poster, date,
0477                          id, references, size, lines)"""
0478 
0479         resp, lines = self.longcmd('XOVER ' + start + '-' + end, file)
0480         xover_lines = []
0481         for line in lines:
0482             elem = line.split("\t")
0483             try:
0484                 xover_lines.append((elem[0],
0485                                     elem[1],
0486                                     elem[2],
0487                                     elem[3],
0488                                     elem[4],
0489                                     elem[5].split(),
0490                                     elem[6],
0491                                     elem[7]))
0492             except IndexError:
0493                 raise NNTPDataError(line)
0494         return resp,xover_lines
0495 
0496     def xgtitle(self, group, file=None):
0497         """Process an XGTITLE command (optional server extension) Arguments:
0498         - group: group name wildcard (i.e. news.*)
0499         Returns:
0500         - resp: server response if successful
0501         - list: list of (name,title) strings"""
0502 
0503         line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
0504         resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
0505         lines = []
0506         for raw_line in raw_lines:
0507             match = line_pat.search(raw_line.strip())
0508             if match:
0509                 lines.append(match.group(1, 2))
0510         return resp, lines
0511 
0512     def xpath(self,id):
0513         """Process an XPATH command (optional server extension) Arguments:
0514         - id: Message id of article
0515         Returns:
0516         resp: server response if successful
0517         path: directory path to article"""
0518 
0519         resp = self.shortcmd("XPATH " + id)
0520         if resp[:3] != '223':
0521             raise NNTPReplyError(resp)
0522         try:
0523             [resp_num, path] = resp.split()
0524         except ValueError:
0525             raise NNTPReplyError(resp)
0526         else:
0527             return resp, path
0528 
0529     def date (self):
0530         """Process the DATE command. Arguments:
0531         None
0532         Returns:
0533         resp: server response if successful
0534         date: Date suitable for newnews/newgroups commands etc.
0535         time: Time suitable for newnews/newgroups commands etc."""
0536 
0537         resp = self.shortcmd("DATE")
0538         if resp[:3] != '111':
0539             raise NNTPReplyError(resp)
0540         elem = resp.split()
0541         if len(elem) != 2:
0542             raise NNTPDataError(resp)
0543         date = elem[1][2:8]
0544         time = elem[1][-6:]
0545         if len(date) != 6 or len(time) != 6:
0546             raise NNTPDataError(resp)
0547         return resp, date, time
0548 
0549 
0550     def post(self, f):
0551         """Process a POST command.  Arguments:
0552         - f: file containing the article
0553         Returns:
0554         - resp: server response if successful"""
0555 
0556         resp = self.shortcmd('POST')
0557         # Raises error_??? if posting is not allowed
0558         if resp[0] != '3':
0559             raise NNTPReplyError(resp)
0560         while 1:
0561             line = f.readline()
0562             if not line:
0563                 break
0564             if line[-1] == '\n':
0565                 line = line[:-1]
0566             if line[:1] == '.':
0567                 line = '.' + line
0568             self.putline(line)
0569         self.putline('.')
0570         return self.getresp()
0571 
0572     def ihave(self, id, f):
0573         """Process an IHAVE command.  Arguments:
0574         - id: message-id of the article
0575         - f:  file containing the article
0576         Returns:
0577         - resp: server response if successful
0578         Note that if the server refuses the article an exception is raised."""
0579 
0580         resp = self.shortcmd('IHAVE ' + id)
0581         # Raises error_??? if the server already has it
0582         if resp[0] != '3':
0583             raise NNTPReplyError(resp)
0584         while 1:
0585             line = f.readline()
0586             if not line:
0587                 break
0588             if line[-1] == '\n':
0589                 line = line[:-1]
0590             if line[:1] == '.':
0591                 line = '.' + line
0592             self.putline(line)
0593         self.putline('.')
0594         return self.getresp()
0595 
0596     def quit(self):
0597         """Process a QUIT command and close the socket.  Returns:
0598         - resp: server response if successful"""
0599 
0600         resp = self.shortcmd('QUIT')
0601         self.file.close()
0602         self.sock.close()
0603         del self.file, self.sock
0604         return resp
0605 
0606 
0607 # Test retrieval when run as a script.
0608 # Assumption: if there's a local news server, it's called 'news'.
0609 # Assumption: if user queries a remote news server, it's named
0610 # in the environment variable NNTPSERVER (used by slrn and kin)
0611 # and we want readermode off.
0612 if __name__ == '__main__':
0613     import os
0614     newshost = 'news' and os.environ["NNTPSERVER"]
0615     if newshost.find('.') == -1:
0616         mode = 'readermode'
0617     else:
0618         mode = None
0619     s = NNTP(newshost, readermode=mode)
0620     resp, count, first, last, name = s.group('comp.lang.python')
0621     print resp
0622     print 'Group', name, 'has', count, 'articles, range', first, 'to', last
0623     resp, subs = s.xhdr('subject', first + '-' + last)
0624     print resp
0625     for item in subs:
0626         print "%7s %s" % item
0627     resp = s.quit()
0628     print resp
0629 

Generated by PyXR 0.9.4
SourceForge.net Logo