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