nntplib.py 21 KB

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