]> rtime.felk.cvut.cz Git - l4.git/blob - l4/pkg/python/contrib/Lib/nntplib.py
Inital import
[l4.git] / l4 / pkg / python / contrib / Lib / nntplib.py
1 """An NNTP client class based on RFC 977: Network News Transfer Protocol.
2
3 Example:
4
5 >>> from nntplib import NNTP
6 >>> s = NNTP('news')
7 >>> resp, count, first, last, name = s.group('comp.lang.python')
8 >>> print 'Group', name, 'has', count, 'articles, range', first, 'to', last
9 Group comp.lang.python has 51 articles, range 5770 to 5821
10 >>> resp, subs = s.xhdr('subject', first + '-' + last)
11 >>> resp = s.quit()
12 >>>
13
14 Here 'resp' is the server response line.
15 Error responses are turned into exceptions.
16
17 To post an article from a file:
18 >>> f = open(filename, 'r') # file containing article, including header
19 >>> resp = s.post(f)
20 >>>
21
22 For descriptions of all methods, read the comments in the code below.
23 Note that all arguments and return values representing article numbers
24 are strings, not numbers, since they are rarely used for calculations.
25 """
26
27 # RFC 977 by Brian Kantor and Phil Lapsley.
28 # xover, xgtitle, xpath, date methods by Kevan Heydon
29
30
31 # Imports
32 import re
33 import socket
34
35 __all__ = ["NNTP","NNTPReplyError","NNTPTemporaryError",
36            "NNTPPermanentError","NNTPProtocolError","NNTPDataError",
37            "error_reply","error_temp","error_perm","error_proto",
38            "error_data",]
39
40 # Exceptions raised when an error or invalid response is received
41 class NNTPError(Exception):
42     """Base class for all nntplib exceptions"""
43     def __init__(self, *args):
44         Exception.__init__(self, *args)
45         try:
46             self.response = args[0]
47         except IndexError:
48             self.response = 'No response given'
49
50 class NNTPReplyError(NNTPError):
51     """Unexpected [123]xx reply"""
52     pass
53
54 class NNTPTemporaryError(NNTPError):
55     """4xx errors"""
56     pass
57
58 class NNTPPermanentError(NNTPError):
59     """5xx errors"""
60     pass
61
62 class NNTPProtocolError(NNTPError):
63     """Response does not begin with [1-5]"""
64     pass
65
66 class NNTPDataError(NNTPError):
67     """Error in response data"""
68     pass
69
70 # for backwards compatibility
71 error_reply = NNTPReplyError
72 error_temp = NNTPTemporaryError
73 error_perm = NNTPPermanentError
74 error_proto = NNTPProtocolError
75 error_data = NNTPDataError
76
77
78
79 # Standard port used by NNTP servers
80 NNTP_PORT = 119
81
82
83 # Response numbers that are followed by additional text (e.g. article)
84 LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282']
85
86
87 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
88 CRLF = '\r\n'
89
90
91
92 # The class itself
93 class NNTP:
94     def __init__(self, host, port=NNTP_PORT, user=None, password=None,
95                  readermode=None, usenetrc=True):
96         """Initialize an instance.  Arguments:
97         - host: hostname to connect to
98         - port: port to connect to (default the standard NNTP port)
99         - user: username to authenticate with
100         - password: password to use with username
101         - readermode: if true, send 'mode reader' command after
102                       connecting.
103
104         readermode is sometimes necessary if you are connecting to an
105         NNTP server on the local machine and intend to call
106         reader-specific comamnds, such as `group'.  If you get
107         unexpected NNTPPermanentErrors, you might need to set
108         readermode.
109         """
110         self.host = host
111         self.port = port
112         self.sock = socket.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