Home | History | Annotate | Download | only in python2.7
      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 commands, 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.create_connection((host, port))
    113         self.file = self.sock.makefile('rb')
    114         self.debugging = 0
    115         self.welcome = self.getresp()
    116 
    117         # 'mode reader' is sometimes necessary to enable 'reader' mode.
    118         # However, the order in which 'mode reader' and 'authinfo' need to
    119         # arrive differs between some NNTP servers. Try to send
    120         # 'mode reader', and if it fails with an authorization failed
    121         # error, try again after sending authinfo.
    122         readermode_afterauth = 0
    123         if readermode:
    124             try:
    125                 self.welcome = self.shortcmd('mode reader')
    126             except NNTPPermanentError:
    127                 # error 500, probably 'not implemented'
    128                 pass
    129             except NNTPTemporaryError, e:
    130                 if user and e.response[:3] == '480':
    131                     # Need authorization before 'mode reader'
    132                     readermode_afterauth = 1
    133                 else:
    134                     raise
    135         # If no login/password was specified, try to get them from ~/.netrc
    136         # Presume that if .netc has an entry, NNRP authentication is required.
    137         try:
    138             if usenetrc and not user:
    139                 import netrc
    140                 credentials = netrc.netrc()
    141                 auth = credentials.authenticators(host)
    142                 if auth:
    143                     user = auth[0]
    144                     password = auth[2]
    145         except IOError:
    146             pass
    147         # Perform NNRP authentication if needed.
    148         if user:
    149             resp = self.shortcmd('authinfo user '+user)
    150             if resp[:3] == '381':
    151                 if not password:
    152                     raise NNTPReplyError(resp)
    153                 else:
    154                     resp = self.shortcmd(
    155                             'authinfo pass '+password)
    156                     if resp[:3] != '281':
    157                         raise NNTPPermanentError(resp)
    158             if readermode_afterauth:
    159                 try:
    160                     self.welcome = self.shortcmd('mode reader')
    161                 except NNTPPermanentError:
    162                     # error 500, probably 'not implemented'
    163                     pass
    164 
    165 
    166     # Get the welcome message from the server
    167     # (this is read and squirreled away by __init__()).
    168     # If the response code is 200, posting is allowed;
    169     # if it 201, posting is not allowed
    170 
    171     def getwelcome(self):
    172         """Get the welcome message from the server
    173         (this is read and squirreled away by __init__()).
    174         If the response code is 200, posting is allowed;
    175         if it 201, posting is not allowed."""
    176 
    177         if self.debugging: print '*welcome*', repr(self.welcome)
    178         return self.welcome
    179 
    180     def set_debuglevel(self, level):
    181         """Set the debugging level.  Argument 'level' means:
    182         0: no debugging output (default)
    183         1: print commands and responses but not body text etc.
    184         2: also print raw lines read and sent before stripping CR/LF"""
    185 
    186         self.debugging = level
    187     debug = set_debuglevel
    188 
    189     def putline(self, line):
    190         """Internal: send one line to the server, appending CRLF."""
    191         line = line + CRLF
    192         if self.debugging > 1: print '*put*', repr(line)
    193         self.sock.sendall(line)
    194 
    195     def putcmd(self, line):
    196         """Internal: send one command to the server (through putline())."""
    197         if self.debugging: print '*cmd*', repr(line)
    198         self.putline(line)
    199 
    200     def getline(self):
    201         """Internal: return one line from the server, stripping CRLF.
    202         Raise EOFError if the connection is closed."""
    203         line = self.file.readline()
    204         if self.debugging > 1:
    205             print '*get*', repr(line)
    206         if not line: raise EOFError
    207         if line[-2:] == CRLF: line = line[:-2]
    208         elif line[-1:] in CRLF: line = line[:-1]
    209         return line
    210 
    211     def getresp(self):
    212         """Internal: get a response from the server.
    213         Raise various errors if the response indicates an error."""
    214         resp = self.getline()
    215         if self.debugging: print '*resp*', repr(resp)
    216         c = resp[:1]
    217         if c == '4':
    218             raise NNTPTemporaryError(resp)
    219         if c == '5':
    220             raise NNTPPermanentError(resp)
    221         if c not in '123':
    222             raise NNTPProtocolError(resp)
    223         return resp
    224 
    225     def getlongresp(self, file=None):
    226         """Internal: get a response plus following text from the server.
    227         Raise various errors if the response indicates an error."""
    228 
    229         openedFile = None
    230         try:
    231             # If a string was passed then open a file with that name
    232             if isinstance(file, str):
    233                 openedFile = file = open(file, "w")
    234 
    235             resp = self.getresp()
    236             if resp[:3] not in LONGRESP:
    237                 raise NNTPReplyError(resp)
    238             list = []
    239             while 1:
    240                 line = self.getline()
    241                 if line == '.':
    242                     break
    243                 if line[:2] == '..':
    244                     line = line[1:]
    245                 if file:
    246                     file.write(line + "\n")
    247                 else:
    248                     list.append(line)
    249         finally:
    250             # If this method created the file, then it must close it
    251             if openedFile:
    252                 openedFile.close()
    253 
    254         return resp, list
    255 
    256     def shortcmd(self, line):
    257         """Internal: send a command and get the response."""
    258         self.putcmd(line)
    259         return self.getresp()
    260 
    261     def longcmd(self, line, file=None):
    262         """Internal: send a command and get the response plus following text."""
    263         self.putcmd(line)
    264         return self.getlongresp(file)
    265 
    266     def newgroups(self, date, time, file=None):
    267         """Process a NEWGROUPS command.  Arguments:
    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 newsgroup names"""
    273 
    274         return self.longcmd('NEWGROUPS ' + date + ' ' + time, file)
    275 
    276     def newnews(self, group, date, time, file=None):
    277         """Process a NEWNEWS command.  Arguments:
    278         - group: group name or '*'
    279         - date: string 'yymmdd' indicating the date
    280         - time: string 'hhmmss' indicating the time
    281         Return:
    282         - resp: server response if successful
    283         - list: list of message ids"""
    284 
    285         cmd = 'NEWNEWS ' + group + ' ' + date + ' ' + time
    286         return self.longcmd(cmd, file)
    287 
    288     def list(self, file=None):
    289         """Process a LIST command.  Return:
    290         - resp: server response if successful
    291         - list: list of (group, last, first, flag) (strings)"""
    292 
    293         resp, list = self.longcmd('LIST', file)
    294         for i in range(len(list)):
    295             # Parse lines into "group last first flag"
    296             list[i] = tuple(list[i].split())
    297         return resp, list
    298 
    299     def description(self, group):
    300 
    301         """Get a description for a single group.  If more than one
    302         group matches ('group' is a pattern), return the first.  If no
    303         group matches, return an empty string.
    304 
    305         This elides the response code from the server, since it can
    306         only be '215' or '285' (for xgtitle) anyway.  If the response
    307         code is needed, use the 'descriptions' method.
    308 
    309         NOTE: This neither checks for a wildcard in 'group' nor does
    310         it check whether the group actually exists."""
    311 
    312         resp, lines = self.descriptions(group)
    313         if len(lines) == 0:
    314             return ""
    315         else:
    316             return lines[0][1]
    317 
    318     def descriptions(self, group_pattern):
    319         """Get descriptions for a range of groups."""
    320         line_pat = re.compile("^(?P<group>[^ \t]+)[ \t]+(.*)$")
    321         # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
    322         resp, raw_lines = self.longcmd('LIST NEWSGROUPS ' + group_pattern)
    323         if resp[:3] != "215":
    324             # Now the deprecated XGTITLE.  This either raises an error
    325             # or succeeds with the same output structure as LIST
    326             # NEWSGROUPS.
    327             resp, raw_lines = self.longcmd('XGTITLE ' + group_pattern)
    328         lines = []
    329         for raw_line in raw_lines:
    330             match = line_pat.search(raw_line.strip())
    331             if match:
    332                 lines.append(match.group(1, 2))
    333         return resp, lines
    334 
    335     def group(self, name):
    336         """Process a GROUP command.  Argument:
    337         - group: the group name
    338         Returns:
    339         - resp: server response if successful
    340         - count: number of articles (string)
    341         - first: first article number (string)
    342         - last: last article number (string)
    343         - name: the group name"""
    344 
    345         resp = self.shortcmd('GROUP ' + name)
    346         if resp[:3] != '211':
    347             raise NNTPReplyError(resp)
    348         words = resp.split()
    349         count = first = last = 0
    350         n = len(words)
    351         if n > 1:
    352             count = words[1]
    353             if n > 2:
    354                 first = words[2]
    355                 if n > 3:
    356                     last = words[3]
    357                     if n > 4:
    358                         name = words[4].lower()
    359         return resp, count, first, last, name
    360 
    361     def help(self, file=None):
    362         """Process a HELP command.  Returns:
    363         - resp: server response if successful
    364         - list: list of strings"""
    365 
    366         return self.longcmd('HELP',file)
    367 
    368     def statparse(self, resp):
    369         """Internal: parse the response of a STAT, NEXT or LAST command."""
    370         if resp[:2] != '22':
    371             raise NNTPReplyError(resp)
    372         words = resp.split()
    373         nr = 0
    374         id = ''
    375         n = len(words)
    376         if n > 1:
    377             nr = words[1]
    378             if n > 2:
    379                 id = words[2]
    380         return resp, nr, id
    381 
    382     def statcmd(self, line):
    383         """Internal: process a STAT, NEXT or LAST command."""
    384         resp = self.shortcmd(line)
    385         return self.statparse(resp)
    386 
    387     def stat(self, id):
    388         """Process a STAT command.  Argument:
    389         - id: article number or message id
    390         Returns:
    391         - resp: server response if successful
    392         - nr:   the article number
    393         - id:   the message id"""
    394 
    395         return self.statcmd('STAT ' + id)
    396 
    397     def next(self):
    398         """Process a NEXT command.  No arguments.  Return as for STAT."""
    399         return self.statcmd('NEXT')
    400 
    401     def last(self):
    402         """Process a LAST command.  No arguments.  Return as for STAT."""
    403         return self.statcmd('LAST')
    404 
    405     def artcmd(self, line, file=None):
    406         """Internal: process a HEAD, BODY or ARTICLE command."""
    407         resp, list = self.longcmd(line, file)
    408         resp, nr, id = self.statparse(resp)
    409         return resp, nr, id, list
    410 
    411     def head(self, id):
    412         """Process a HEAD command.  Argument:
    413         - id: article number or message id
    414         Returns:
    415         - resp: server response if successful
    416         - nr: article number
    417         - id: message id
    418         - list: the lines of the article's header"""
    419 
    420         return self.artcmd('HEAD ' + id)
    421 
    422     def body(self, id, file=None):
    423         """Process a BODY command.  Argument:
    424         - id: article number or message id
    425         - file: Filename string or file object to store the article in
    426         Returns:
    427         - resp: server response if successful
    428         - nr: article number
    429         - id: message id
    430         - list: the lines of the article's body or an empty list
    431                 if file was used"""
    432 
    433         return self.artcmd('BODY ' + id, file)
    434 
    435     def article(self, id):
    436         """Process an ARTICLE command.  Argument:
    437         - id: article number or message id
    438         Returns:
    439         - resp: server response if successful
    440         - nr: article number
    441         - id: message id
    442         - list: the lines of the article"""
    443 
    444         return self.artcmd('ARTICLE ' + id)
    445 
    446     def slave(self):
    447         """Process a SLAVE command.  Returns:
    448         - resp: server response if successful"""
    449 
    450         return self.shortcmd('SLAVE')
    451 
    452     def xhdr(self, hdr, str, file=None):
    453         """Process an XHDR command (optional server extension).  Arguments:
    454         - hdr: the header type (e.g. 'subject')
    455         - str: an article nr, a message id, or a range nr1-nr2
    456         Returns:
    457         - resp: server response if successful
    458         - list: list of (nr, value) strings"""
    459 
    460         pat = re.compile('^([0-9]+) ?(.*)\n?')
    461         resp, lines = self.longcmd('XHDR ' + hdr + ' ' + str, file)
    462         for i in range(len(lines)):
    463             line = lines[i]
    464             m = pat.match(line)
    465             if m:
    466                 lines[i] = m.group(1, 2)
    467         return resp, lines
    468 
    469     def xover(self, start, end, file=None):
    470         """Process an XOVER command (optional server extension) Arguments:
    471         - start: start of range
    472         - end: end of range
    473         Returns:
    474         - resp: server response if successful
    475         - list: list of (art-nr, subject, poster, date,
    476                          id, references, size, lines)"""
    477 
    478         resp, lines = self.longcmd('XOVER ' + start + '-' + end, file)
    479         xover_lines = []
    480         for line in lines:
    481             elem = line.split("\t")
    482             try:
    483                 xover_lines.append((elem[0],
    484                                     elem[1],
    485                                     elem[2],
    486                                     elem[3],
    487                                     elem[4],
    488                                     elem[5].split(),
    489                                     elem[6],
    490                                     elem[7]))
    491             except IndexError:
    492                 raise NNTPDataError(line)
    493         return resp,xover_lines
    494 
    495     def xgtitle(self, group, file=None):
    496         """Process an XGTITLE command (optional server extension) Arguments:
    497         - group: group name wildcard (i.e. news.*)
    498         Returns:
    499         - resp: server response if successful
    500         - list: list of (name,title) strings"""
    501 
    502         line_pat = re.compile("^([^ \t]+)[ \t]+(.*)$")
    503         resp, raw_lines = self.longcmd('XGTITLE ' + group, file)
    504         lines = []
    505         for raw_line in raw_lines:
    506             match = line_pat.search(raw_line.strip())
    507             if match:
    508                 lines.append(match.group(1, 2))
    509         return resp, lines
    510 
    511     def xpath(self,id):
    512         """Process an XPATH command (optional server extension) Arguments:
    513         - id: Message id of article
    514         Returns:
    515         resp: server response if successful
    516         path: directory path to article"""
    517 
    518         resp = self.shortcmd("XPATH " + id)
    519         if resp[:3] != '223':
    520             raise NNTPReplyError(resp)
    521         try:
    522             [resp_num, path] = resp.split()
    523         except ValueError:
    524             raise NNTPReplyError(resp)
    525         else:
    526             return resp, path
    527 
    528     def date (self):
    529         """Process the DATE command. Arguments:
    530         None
    531         Returns:
    532         resp: server response if successful
    533         date: Date suitable for newnews/newgroups commands etc.
    534         time: Time suitable for newnews/newgroups commands etc."""
    535 
    536         resp = self.shortcmd("DATE")
    537         if resp[:3] != '111':
    538             raise NNTPReplyError(resp)
    539         elem = resp.split()
    540         if len(elem) != 2:
    541             raise NNTPDataError(resp)
    542         date = elem[1][2:8]
    543         time = elem[1][-6:]
    544         if len(date) != 6 or len(time) != 6:
    545             raise NNTPDataError(resp)
    546         return resp, date, time
    547 
    548 
    549     def post(self, f):
    550         """Process a POST command.  Arguments:
    551         - f: file containing the article
    552         Returns:
    553         - resp: server response if successful"""
    554 
    555         resp = self.shortcmd('POST')
    556         # Raises error_??? if posting is not allowed
    557         if resp[0] != '3':
    558             raise NNTPReplyError(resp)
    559         while 1:
    560             line = f.readline()
    561             if not line:
    562                 break
    563             if line[-1] == '\n':
    564                 line = line[:-1]
    565             if line[:1] == '.':
    566                 line = '.' + line
    567             self.putline(line)
    568         self.putline('.')
    569         return self.getresp()
    570 
    571     def ihave(self, id, f):
    572         """Process an IHAVE command.  Arguments:
    573         - id: message-id of the article
    574         - f:  file containing the article
    575         Returns:
    576         - resp: server response if successful
    577         Note that if the server refuses the article an exception is raised."""
    578 
    579         resp = self.shortcmd('IHAVE ' + id)
    580         # Raises error_??? if the server already has it
    581         if resp[0] != '3':
    582             raise NNTPReplyError(resp)
    583         while 1:
    584             line = f.readline()
    585             if not line:
    586                 break
    587             if line[-1] == '\n':
    588                 line = line[:-1]
    589             if line[:1] == '.':
    590                 line = '.' + line
    591             self.putline(line)
    592         self.putline('.')
    593         return self.getresp()
    594 
    595     def quit(self):
    596         """Process a QUIT command and close the socket.  Returns:
    597         - resp: server response if successful"""
    598 
    599         resp = self.shortcmd('QUIT')
    600         self.file.close()
    601         self.sock.close()
    602         del self.file, self.sock
    603         return resp
    604 
    605 
    606 # Test retrieval when run as a script.
    607 # Assumption: if there's a local news server, it's called 'news'.
    608 # Assumption: if user queries a remote news server, it's named
    609 # in the environment variable NNTPSERVER (used by slrn and kin)
    610 # and we want readermode off.
    611 if __name__ == '__main__':
    612     import os
    613     newshost = 'news' and os.environ["NNTPSERVER"]
    614     if newshost.find('.') == -1:
    615         mode = 'readermode'
    616     else:
    617         mode = None
    618     s = NNTP(newshost, readermode=mode)
    619     resp, count, first, last, name = s.group('comp.lang.python')
    620     print resp
    621     print 'Group', name, 'has', count, 'articles, range', first, 'to', last
    622     resp, subs = s.xhdr('subject', first + '-' + last)
    623     print resp
    624     for item in subs:
    625         print "%7s %s" % item
    626     resp = s.quit()
    627     print resp
    628