root / trunk / twisted / news / nntp.py

Revision 24441, 32.6 kB (checked in by thijs, 1 year ago)

Merge maintainer-email-2438: Get rid of references to maintainer email addresses from code.

Author: thijs
Reviewer: exarkun
Fixes: #2438

Line 
1 # -*- test-case-name: twisted.news.test.test_nntp -*-
2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5
6 """
7 NNTP protocol support.
8
9 Maintainer: Jp Calderone
10
11 The following protocol commands are currently understood::
12
13     LIST        LISTGROUP                  XOVER        XHDR
14     POST        GROUP        ARTICLE       STAT         HEAD
15     BODY        NEXT         MODE STREAM   MODE READER  SLAVE
16     LAST        QUIT         HELP          IHAVE        XPATH
17     XINDEX      XROVER       TAKETHIS      CHECK
18     
19 The following protocol commands require implementation::
20
21                              NEWNEWS
22                              XGTITLE                XPAT
23                              XTHREAD       AUTHINFO NEWGROUPS
24
25
26 Other desired features:
27
28    - A real backend
29    - More robust client input handling
30    - A control protocol
31 """
32
33 import time
34 import types
35
36 try:
37     import cStringIO as StringIO
38 except:
39     import StringIO
40
41 from twisted.protocols import basic
42 from twisted.python import log
43
44 def parseRange(text):
45     articles = text.split('-')
46     if len(articles) == 1:
47         try:
48             a = int(articles[0])
49             return a, a
50         except ValueError, e:
51             return None, None
52     elif len(articles) == 2:
53         try:
54             if len(articles[0]):
55                 l = int(articles[0])
56             else:
57                 l = None
58             if len(articles[1]):
59                 h = int(articles[1])
60             else:
61                 h = None
62         except ValueError, e:
63             return None, None
64     return l, h
65
66
67 def extractCode(line):
68     line = line.split(' ', 1)
69     if len(line) != 2:
70         return None
71     try:
72         return int(line[0]), line[1]
73     except ValueError:
74         return None
75
76    
77 class NNTPError(Exception):
78     def __init__(self, string):
79         self.string = string
80
81     def __str__(self):
82         return 'NNTPError: %s' % self.string
83
84
85 class NNTPClient(basic.LineReceiver):
86     MAX_COMMAND_LENGTH = 510
87
88     def __init__(self):
89         self.currentGroup = None
90        
91         self._state = []
92         self._error = []
93         self._inputBuffers = []
94         self._responseCodes = []
95         self._responseHandlers = []
96        
97         self._postText = []
98        
99         self._newState(self._statePassive, None, self._headerInitial)
100
101
102     def gotAllGroups(self, groups):
103         "Override for notification when fetchGroups() action is completed"
104    
105    
106     def getAllGroupsFailed(self, error):
107         "Override for notification when fetchGroups() action fails"
108
109
110     def gotOverview(self, overview):
111         "Override for notification when fetchOverview() action is completed"
112
113
114     def getOverviewFailed(self, error):
115         "Override for notification when fetchOverview() action fails"
116
117
118     def gotSubscriptions(self, subscriptions):
119         "Override for notification when fetchSubscriptions() action is completed"
120
121
122     def getSubscriptionsFailed(self, error):
123         "Override for notification when fetchSubscriptions() action fails"
124
125
126     def gotGroup(self, group):
127         "Override for notification when fetchGroup() action is completed"
128
129
130     def getGroupFailed(self, error):
131         "Override for notification when fetchGroup() action fails"
132
133
134     def gotArticle(self, article):
135         "Override for notification when fetchArticle() action is completed"
136
137
138     def getArticleFailed(self, error):
139         "Override for notification when fetchArticle() action fails"
140
141
142     def gotHead(self, head):
143         "Override for notification when fetchHead() action is completed"
144
145
146     def getHeadFailed(self, error):
147         "Override for notification when fetchHead() action fails"
148
149
150     def gotBody(self, info):
151         "Override for notification when fetchBody() action is completed"
152
153
154     def getBodyFailed(self, body):
155         "Override for notification when fetchBody() action fails"
156
157
158     def postedOk(self):
159         "Override for notification when postArticle() action is successful"
160
161    
162     def postFailed(self, error):
163         "Override for notification when postArticle() action fails"
164
165
166     def gotXHeader(self, headers):
167         "Override for notification when getXHeader() action is successful"
168    
169    
170     def getXHeaderFailed(self, error):
171         "Override for notification when getXHeader() action fails"
172
173
174     def gotNewNews(self, news):
175         "Override for notification when getNewNews() action is successful"
176    
177    
178     def getNewNewsFailed(self, error):
179         "Override for notification when getNewNews() action fails"
180
181
182     def gotNewGroups(self, groups):
183         "Override for notification when getNewGroups() action is successful"
184    
185    
186     def getNewGroupsFailed(self, error):
187         "Override for notification when getNewGroups() action fails"
188
189
190     def setStreamSuccess(self):
191         "Override for notification when setStream() action is successful"
192
193
194     def setStreamFailed(self, error):
195         "Override for notification when setStream() action fails"
196
197
198     def fetchGroups(self):
199         """
200         Request a list of all news groups from the server.  gotAllGroups()
201         is called on success, getGroupsFailed() on failure
202         """
203         self.sendLine('LIST')
204         self._newState(self._stateList, self.getAllGroupsFailed)
205
206
207     def fetchOverview(self):
208         """
209         Request the overview format from the server.  gotOverview() is called
210         on success, getOverviewFailed() on failure
211         """
212         self.sendLine('LIST OVERVIEW.FMT')
213         self._newState(self._stateOverview, self.getOverviewFailed)
214
215
216     def fetchSubscriptions(self):
217         """
218         Request a list of the groups it is recommended a new user subscribe to.
219         gotSubscriptions() is called on success, getSubscriptionsFailed() on
220         failure
221         """
222         self.sendLine('LIST SUBSCRIPTIONS')
223         self._newState(self._stateSubscriptions, self.getSubscriptionsFailed)
224
225
226     def fetchGroup(self, group):
227         """
228         Get group information for the specified group from the server.  gotGroup()
229         is called on success, getGroupFailed() on failure.
230         """
231         self.sendLine('GROUP %s' % (group,))
232         self._newState(None, self.getGroupFailed, self._headerGroup)
233
234
235     def fetchHead(self, index = ''):
236         """
237         Get the header for the specified article (or the currently selected
238         article if index is '') from the server.  gotHead() is called on
239         success, getHeadFailed() on failure
240         """
241         self.sendLine('HEAD %s' % (index,))
242         self._newState(self._stateHead, self.getHeadFailed)
243
244        
245     def fetchBody(self, index = ''):
246         """
247         Get the body for the specified article (or the currently selected
248         article if index is '') from the server.  gotBody() is called on
249         success, getBodyFailed() on failure
250         """
251         self.sendLine('BODY %s' % (index,))
252         self._newState(self._stateBody, self.getBodyFailed)
253
254
255     def fetchArticle(self, index = ''):
256         """
257         Get the complete article with the specified index (or the currently
258         selected article if index is '') or Message-ID from the server.
259         gotArticle() is called on success, getArticleFailed() on failure.
260         """
261         self.sendLine('ARTICLE %s' % (index,))
262         self._newState(self._stateArticle, self.getArticleFailed)
263
264
265     def postArticle(self, text):
266         """
267         Attempt to post an article with the specified text to the server.  'text'
268         must consist of both head and body data, as specified by RFC 850.  If the
269         article is posted successfully, postedOk() is called, otherwise postFailed()
270         is called.
271         """
272         self.sendLine('POST')
273         self._newState(None, self.postFailed, self._headerPost)
274         self._postText.append(text)
275
276
277     def fetchNewNews(self, groups, date, distributions = ''):
278         """
279         Get the Message-IDs for all new news posted to any of the given
280         groups since the specified date - in seconds since the epoch, GMT -
281         optionally restricted to the given distributions.  gotNewNews() is
282         called on success, getNewNewsFailed() on failure.
283         
284         One invocation of this function may result in multiple invocations
285         of gotNewNews()/getNewNewsFailed().
286         """
287         date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
288         line = 'NEWNEWS %%s %s %s %s' % (date, timeStr, distributions)
289         groupPart = ''
290         while len(groups) and len(line) + len(groupPart) + len(groups[-1]) + 1 < NNTPClient.MAX_COMMAND_LENGTH:
291             group = groups.pop()
292             groupPart = groupPart + ',' + group
293        
294         self.sendLine(line % (groupPart,))
295         self._newState(self._stateNewNews, self.getNewNewsFailed)
296        
297         if len(groups):
298             self.fetchNewNews(groups, date, distributions)
299    
300    
301     def fetchNewGroups(self, date, distributions):
302         """
303         Get the names of all new groups created/added to the server since
304         the specified date - in seconds since the ecpoh, GMT - optionally
305         restricted to the given distributions.  gotNewGroups() is called
306         on success, getNewGroupsFailed() on failure.
307         """
308         date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
309         self.sendLine('NEWGROUPS %s %s %s' % (date, timeStr, distributions))
310         self._newState(self._stateNewGroups, self.getNewGroupsFailed)
311
312
313     def fetchXHeader(self, header, low = None, high = None, id = None):
314         """
315         Request a specific header from the server for an article or range
316         of articles.  If 'id' is not None, a header for only the article
317         with that Message-ID will be requested.  If both low and high are
318         None, a header for the currently selected article will be selected;
319         If both low and high are zero-length strings, headers for all articles
320         in the currently selected group will be requested;  Otherwise, high
321         and low will be used as bounds - if one is None the first or last
322         article index will be substituted, as appropriate.
323         """
324         if id is not None:
325             r = header + ' <%s>' % (id,)
326         elif low is high is None:
327             r = header
328         elif high is None:
329             r = header + ' %d-' % (low,)
330         elif low is None:
331             r = header + ' -%d' % (high,)
332         else:
333             r = header + ' %d-%d' % (low, high)
334         self.sendLine('XHDR ' + r)
335         self._newState(self._stateXHDR, self.getXHeaderFailed)
336
337
338     def setStream(self):
339         """
340         Set the mode to STREAM, suspending the normal "lock-step" mode of
341         communications.  setStreamSuccess() is called on success,
342         setStreamFailed() on failure.
343         """
344         self.sendLine('MODE STREAM')
345         self._newState(None, self.setStreamFailed, self._headerMode)
346
347
348     def quit(self):
349         self.sendLine('QUIT')
350         self.transport.loseConnection()
351
352
353     def _newState(self, method, error, responseHandler = None):
354         self._inputBuffers.append([])
355         self._responseCodes.append(None)
356         self._state.append(method)
357         self._error.append(error)
358         self._responseHandlers.append(responseHandler)
359
360
361     def _endState(self):
362         buf = self._inputBuffers[0]
363         del self._responseCodes[0]
364         del self._inputBuffers[0]
365         del self._state[0]
366         del self._error[0]
367         del self._responseHandlers[0]
368         return buf
369
370
371     def _newLine(self, line, check = 1):
372         if check and line and line[0] == '.':
373             line = line[1:]
374         self._inputBuffers[0].append(line)
375
376
377     def _setResponseCode(self, code):
378         self._responseCodes[0] = code
379    
380    
381     def _getResponseCode(self):
382         return self._responseCodes[0]
383
384
385     def lineReceived(self, line):
386         if not len(self._state):
387             self._statePassive(line)
388         elif self._getResponseCode() is None:
389             code = extractCode(line)
390             if code is None or not (200 <= code[0] < 400):    # An error!
391                 self._error[0](line)
392                 self._endState()
393             else:
394                 self._setResponseCode(code)
395                 if self._responseHandlers[0]:
396                     self._responseHandlers[0](code)
397         else:
398             self._state[0](line)
399
400
401     def _statePassive(self, line):
402         log.msg('Server said: %s' % line)
403
404
405     def _passiveError(self, error):
406         log.err('Passive Error: %s' % (error,))
407
408
409     def _headerInitial(self, (code, message)):
410         if code == 200:
411             self.canPost = 1
412         else:
413             self.canPost = 0
414         self._endState()
415
416
417     def _stateList(self, line):
418         if line != '.':
419             data = filter(None, line.strip().split())
420             self._newLine((data[0], int(data[1]), int(data[2]), data[3]), 0)
421         else:
422             self.gotAllGroups(self._endState())
423
424
425     def _stateOverview(self, line):
426         if line != '.':
427             self._newLine(filter(None, line.strip().split()), 0)
428         else:
429             self.gotOverview(self._endState())
430
431
432     def _stateSubscriptions(self, line):
433         if line != '.':
434             self._newLine(line.strip(), 0)
435         else:
436             self.gotSubscriptions(self._endState())
437
438
439     def _headerGroup(self, (code, line)):
440         self.gotGroup(tuple(line.split()))
441         self._endState()
442
443
444     def _stateArticle(self, line):
445         if line != '.':
446             if line.startswith('.'):
447                 line = line[1:]
448             self._newLine(line, 0)
449         else:
450             self.gotArticle('\n'.join(self._endState())+'\n')
451
452
453     def _stateHead(self, line):
454         if line != '.':
455             self._newLine(line, 0)
456         else:
457             self.gotHead('\n'.join(self._endState()))
458
459
460     def _stateBody(self, line):
461         if line != '.':
462             if line.startswith('.'):
463                 line = line[1:]
464             self._newLine(line, 0)
465         else:
466             self.gotBody('\n'.join(self._endState())+'\n')
467
468
469     def _headerPost(self, (code, message)):
470         if code == 340:
471             self.transport.write(self._postText[0].replace('\n', '\r\n').replace('\r\n.', '\r\n..'))
472             if self._postText[0][-1:] != '\n':
473                 self.sendLine('')
474             self.sendLine('.')
475             del self._postText[0]
476             self._newState(None, self.postFailed, self._headerPosted)
477         else:
478             self.postFailed('%d %s' % (code, message))
479         self._endState()
480
481
482     def _headerPosted(self, (code, message)):
483         if code == 240:
484             self.postedOk()
485         else:
486             self.postFailed('%d %s' % (code, message))
487         self._endState()
488
489
490     def _stateXHDR(self, line):
491         if line != '.':
492             self._newLine(line.split(), 0)
493         else:
494             self._gotXHeader(self._endState())
495    
496    
497     def _stateNewNews(self, line):
498         if line != '.':
499             self._newLine(line, 0)
500         else:
501             self.gotNewNews(self._endState())
502    
503    
504     def _stateNewGroups(self, line):
505         if line != '.':
506             self._newLine(line, 0)
507         else:
508             self.gotNewGroups(self._endState())
509
510
511     def _headerMode(self, (code, message)):
512         if code == 203:
513             self.setStreamSuccess()
514         else:
515             self.setStreamFailed((code, message))
516         self._endState()
517
518
519 class NNTPServer(basic.LineReceiver):
520     COMMANDS = [
521         'LIST', 'GROUP', 'ARTICLE', 'STAT', 'MODE', 'LISTGROUP', 'XOVER',
522         'XHDR', 'HEAD', 'BODY', 'NEXT', 'LAST', 'POST', 'QUIT', 'IHAVE',
523         'HELP', 'SLAVE', 'XPATH', 'XINDEX', 'XROVER', 'TAKETHIS', 'CHECK'
524     ]
525
526     def __init__(self):
527         self.servingSlave = 0
528
529
530     def connectionMade(self):
531         self.inputHandler = None
532         self.currentGroup = None
533         self.currentIndex = None
534         self.sendLine('200 server ready - posting allowed')
535
536     def lineReceived(self, line):
537         if self.inputHandler is not None:
538             self.inputHandler(line)
539         else:
540             parts = line.strip().split()
541             if len(parts):
542                 cmd, parts = parts[0].upper(), parts[1:]
543                 if cmd in NNTPServer.COMMANDS:
544                     func = getattr(self, 'do_%s' % cmd)
545                     try:
546                         func(*parts)
547                     except TypeError:
548                         self.sendLine('501 command syntax error')
549                         log.msg("501 command syntax error")
550                         log.msg("command was", line)
551                         log.deferr()
552                     except:
553                         self.sendLine('503 program fault - command not performed')
554                         log.msg("503 program fault")
555                         log.msg("command was", line)
556                         log.deferr()
557                 else:
558                     self.sendLine('500 command not recognized')
559
560
561     def do_LIST(self, subcmd = '', *dummy):
562         subcmd = subcmd.strip().lower()
563         if subcmd == 'newsgroups':
564             # XXX - this could use a real implementation, eh?
565             self.sendLine('215 Descriptions in form "group description"')
566             self.sendLine('.')
567         elif subcmd == 'overview.fmt':
568             defer = self.factory.backend.overviewRequest()
569             defer.addCallbacks(self._gotOverview, self._errOverview)
570             log.msg('overview')
571         elif subcmd == 'subscriptions':
572             defer = self.factory.backend.subscriptionRequest()
573             defer.addCallbacks(self._gotSubscription, self._errSubscription)
574             log.msg('subscriptions')
575         elif subcmd == '':
576             defer = self.factory.backend.listRequest()
577             defer.addCallbacks(self._gotList, self._errList)
578         else:
579             self.sendLine('500 command not recognized')
580
581
582     def _gotList(self, list):
583         self.sendLine('215 newsgroups in form "group high low flags"')
584         for i in list:
585             self.sendLine('%s %d %d %s' % tuple(i))
586         self.sendLine('.')
587
588
589     def _errList(self, failure):
590         print 'LIST failed: ', failure
591         self.sendLine('503 program fault - command not performed')
592
593
594     def _gotSubscription(self, parts):
595         self.sendLine('215 information follows')
596         for i in parts:
597             self.sendLine(i)
598         self.sendLine('.')
599
600
601     def _errSubscription(self, failure):
602         print 'SUBSCRIPTIONS failed: ', failure
603         self.sendLine('503 program fault - comand not performed')
604
605
606     def _gotOverview(self, parts):
607         self.sendLine('215 Order of fields in overview database.')
608         for i in parts:
609             self.sendLine(i + ':')
610         self.sendLine('.')
611
612
613     def _errOverview(self, failure):
614         print 'LIST OVERVIEW.FMT failed: ', failure
615         self.sendLine('503 program fault - command not performed')
616
617
618     def do_LISTGROUP(self, group = None):
619         group = group or self.currentGroup
620         if group is None:
621             self.sendLine('412 Not currently in newsgroup')
622         else:
623             defer = self.factory.backend.listGroupRequest(group)
624             defer.addCallbacks(self._gotListGroup, self._errListGroup)
625
626
627     def _gotListGroup(self, (group, articles)):
628         self.currentGroup = group
629         if len(articles):
630             self.currentIndex = int(articles[0])
631         else:
632             self.currentIndex = None
633
634         self.sendLine('211 list of article numbers follow')
635         for i in articles:
636             self.sendLine(str(i))
637         self.sendLine('.')
638
639
640     def _errListGroup(self, failure):
641         print 'LISTGROUP failed: ', failure
642         self.sendLine('502 no permission')
643
644
645     def do_XOVER(self, range):
646         if self.currentGroup is None:
647             self.sendLine('412 No news group currently selected')
648         else:
649             l, h = parseRange(range)
650             defer = self.factory.backend.xoverRequest(self.currentGroup, l, h)
651             defer.addCallbacks(self._gotXOver, self._errXOver)
652
653
654     def _gotXOver(self, parts):
655         self.sendLine('224 Overview information follows')
656         for i in parts:
657             self.sendLine('\t'.join(map(str, i)))
658         self.sendLine('.')
659
660
661     def _errXOver(self, failure):
662         print 'XOVER failed: ', failure
663         self.sendLine('420 No article(s) selected')
664
665
666     def xhdrWork(self, header, range):
667         if self.currentGroup is None:
668             self.sendLine('412 No news group currently selected')
669         else:
670             if range is None:
671                 if self.currentIndex is None:
672                     self.sendLine('420 No current article selected')
673                     return
674                 else:
675                     l = h = self.currentIndex
676             else:
677                 # FIXME: articles may be a message-id
678                 l, h = parseRange(range)
679            
680             if l is h is None:
681                 self.sendLine('430 no such article')
682             else:
683                 return self.factory.backend.xhdrRequest(self.currentGroup, l, h, header)
684
685
686     def do_XHDR(self, header, range = None):
687         d = self.xhdrWork(header, range)
688         if d:
689             d.addCallbacks(self._gotXHDR, self._errXHDR)
690
691
692     def _gotXHDR(self, parts):
693         self.sendLine('221 Header follows')
694         for i in parts:
695             self.sendLine('%d %s' % i)
696         self.sendLine('.')
697
698     def _errXHDR(self, failure):
699         print 'XHDR failed: ', failure
700         self.sendLine('502 no permission')
701
702
703     def do_XROVER(self, header, range = None):
704         d = self.xhdrWork(header, range)
705         if d:
706             d.addCallbacks(self._gotXROVER, self._errXROVER)
707    
708    
709     def _gotXROVER(self, parts):
710         self.sendLine('224 Overview information follows')
711         for i in parts:
712             self.sendLine('%d %s' % i)
713         self.sendLine('.')
714
715
716     def _errXROVER(self, failure):
717         print 'XROVER failed: ',
718         self._errXHDR(failure)
719
720
721     def do_POST(self):
722         self.inputHandler = self._doingPost
723         self.message = ''
724         self.sendLine('340 send article to be posted.  End with <CR-LF>.<CR-LF>')
725
726
727     def _doingPost(self, line):
728         if line == '.':
729             self.inputHandler = None
730             group, article = self.currentGroup, self.message
731             self.message = ''
732
733             defer = self.factory.backend.postRequest(article)
734             defer.addCallbacks(self._gotPost, self._errPost)
735         else:
736             self.message = self.message + line + '\r\n'
737
738
739     def _gotPost(self, parts):
740         self.sendLine('240 article posted ok')
741        
742    
743     def _errPost(self, failure):
744         print 'POST failed: ', failure
745         self.sendLine('441 posting failed')
746
747
748     def do_CHECK(self, id):
749         d = self.factory.backend.articleExistsRequest(id)
750         d.addCallbacks(self._gotCheck, self._errCheck)
751    
752    
753     def _gotCheck(self, result):
754         if result:
755             self.sendLine("438 already have it, please don't send it to me")
756         else:
757             self.sendLine('238 no such article found, please send it to me')
758    
759    
760     def _errCheck(self, failure):
761         print 'CHECK failed: ', failure
762         self.sendLine('431 try sending it again later')
763
764
765     def do_TAKETHIS(self, id):
766         self.inputHandler = self._doingTakeThis
767         self.message = ''
768    
769    
770     def _doingTakeThis(self, line):
771         if line == '.':
772             self.inputHandler = None
773             article = self.message
774             self.message = ''
775             d = self.factory.backend.postRequest(article)
776             d.addCallbacks(self._didTakeThis, self._errTakeThis)
777         else:
778             self.message = self.message + line + '\r\n'
779
780
781     def _didTakeThis(self, result):
782         self.sendLine('239 article transferred ok')
783    
784    
785     def _errTakeThis(self, failure):
786         print 'TAKETHIS failed: ', failure
787         self.sendLine('439 article transfer failed')
788
789
790     def do_GROUP(self, group):
791         defer = self.factory.backend.groupRequest(group)
792         defer.addCallbacks(self._gotGroup, self._errGroup)
793
794    
795     def _gotGroup(self, (name, num, high, low, flags)):
796         self.currentGroup = name
797         self.currentIndex = low
798         self.sendLine('211 %d %d %d %s group selected' % (num, low, high, name))
799    
800    
801     def _errGroup(self, failure):
802         print 'GROUP failed: ', failure
803         self.sendLine('411 no such group')
804
805
806     def articleWork(self, article, cmd, func):
807         if self.currentGroup is None:
808             self.sendLine('412 no newsgroup has been selected')
809         else:
810             if not article:
811                 if self.currentIndex is None:
812                     self.sendLine('420 no current article has been selected')
813                 else:
814                     article = self.currentIndex
815             else:
816                 if article[0] == '<':
817                     return func(self.currentGroup, index = None, id = article)
818                 else:
819                     try:
820                         article = int(article)
821                         return func(self.currentGroup, article)
822                     except ValueError, e:
823                         self.sendLine('501 command syntax error')
824
825
826     def do_ARTICLE(self, article = None):
827         defer = self.articleWork(article, 'ARTICLE', self.factory.backend.articleRequest)
828         if defer:
829             defer.addCallbacks(self._gotArticle, self._errArticle)
830
831
832     def _gotArticle(self, (index, id, article)):
833         if isinstance(article, types.StringType):
834             import warnings
835             warnings.warn(
836                 "Returning the article as a string from `articleRequest' "
837                 "is deprecated.  Return a file-like object instead."
838             )
839             article = StringIO.StringIO(article)
840         self.currentIndex = index
841         self.sendLine('220 %d %s article' % (index, id))
842         s = basic.FileSender()
843         d = s.beginFileTransfer(article, self.transport)
844         d.addCallback(self.finishedFileTransfer)
845    
846     ##   
847     ## Helper for FileSender
848     ##
849     def finishedFileTransfer(self, lastsent):
850         if lastsent != '\n':
851             line = '\r\n.'
852         else:
853             line = '.'
854         self.sendLine(line)
855     ##
856
857     def _errArticle(self, failure):
858         print 'ARTICLE failed: ', failure
859         self.sendLine('423 bad article number')
860
861
862     def do_STAT(self, article = None):
863         defer = self.articleWork(article, 'STAT', self.factory.backend.articleRequest)
864         if defer:
865             defer.addCallbacks(self._gotStat, self._errStat)
866    
867    
868     def _gotStat(self, (index, id, article)):
869         self.currentIndex = index
870         self.sendLine('223 %d %s article retreived - request text separately' % (index, id))
871
872
873     def _errStat(self, failure):
874         print 'STAT failed: ', failure
875         self.sendLine('423 bad article number')
876
877
878     def do_HEAD(self, article = None):
879         defer = self.articleWork(article, 'HEAD', self.factory.backend.headRequest)
880         if defer:
881             defer.addCallbacks(self._gotHead, self._errHead)
882    
883    
884     def _gotHead(self, (index, id, head)):
885         self.currentIndex = index
886         self.sendLine('221 %d %s article retrieved' % (index, id))
887         self.transport.write(head + '\r\n')
888         self.sendLine('.')
889    
890    
891     def _errHead(self, failure):
892         print 'HEAD failed: ', failure
893         self.sendLine('423 no such article number in this group')
894
895
896     def do_BODY(self, article):
897         defer = self.articleWork(article, 'BODY', self.factory.backend.bodyRequest)
898         if defer:
899             defer.addCallbacks(self._gotBody, self._errBody)
900
901
902     def _gotBody(self, (index, id, body)):
903         if isinstance(body, types.StringType):
904             import warnings
905             warnings.warn(
906                 "Returning the article as a string from `articleRequest' "
907                 "is deprecated.  Return a file-like object instead."
908             )
909             body = StringIO.StringIO(body)
910         self.currentIndex = index
911         self.sendLine('221 %d %s article retrieved' % (index, id))
912         self.lastsent = ''
913         s = basic.FileSender()
914         d = s.beginFileTransfer(body, self.transport)
915         d.addCallback(self.finishedFileTransfer)
916
917     def _errBody(self, failure):
918         print 'BODY failed: ', failure
919         self.sendLine('423 no such article number in this group')
920
921
922     # NEXT and LAST are just STATs that increment currentIndex first.
923     # Accordingly, use the STAT callbacks.
924     def do_NEXT(self):
925         i = self.currentIndex + 1
926         defer = self.factory.backend.articleRequest(self.currentGroup, i)
927         defer.addCallbacks(self._gotStat, self._errStat)
928
929
930     def do_LAST(self):
931         i = self.currentIndex - 1
932         defer = self.factory.backend.articleRequest(self.currentGroup, i)
933         defer.addCallbacks(self._gotStat, self._errStat)
934
935
936     def do_MODE(self, cmd):
937         cmd = cmd.strip().upper()
938         if cmd == 'READER':
939             self.servingSlave = 0
940             self.sendLine('200 Hello, you can post')
941         elif cmd == 'STREAM':
942             self.sendLine('500 Command not understood')
943         else:
944             # This is not a mistake
945             self.sendLine('500 Command not understood')
946
947
948     def do_QUIT(self):
949         self.sendLine('205 goodbye')
950         self.transport.loseConnection()
951
952    
953     def do_HELP(self):
954         self.sendLine('100 help text follows')
955         self.sendLine('Read the RFC.')
956         self.sendLine('.')
957    
958    
959     def do_SLAVE(self):
960         self.sendLine('202 slave status noted')
961         self.servingeSlave = 1
962
963
964     def do_XPATH(self, article):
965         # XPATH is a silly thing to have.  No client has the right to ask
966         # for this piece of information from me, and so that is what I'll
967         # tell them.
968         self.sendLine('502 access restriction or permission denied')
969
970
971     def do_XINDEX(self, article):
972         # XINDEX is another silly command.  The RFC suggests it be relegated
973         # to the history books, and who am I to disagree?
974         self.sendLine('502 access restriction or permission denied')
975
976
977     def do_XROVER(self, range = None):
978         self.do_XHDR(self, 'References', range)
979
980
981     def do_IHAVE(self, id):
982         self.factory.backend.articleExistsRequest(id).addCallback(self._foundArticle)
983
984    
985     def _foundArticle(self, result):
986         if result:
987             self.sendLine('437 article rejected - do not try again')
988         else:
989             self.sendLine('335 send article to be transferred.  End with <CR-LF>.<CR-LF>')
990             self.inputHandler = self._handleIHAVE
991             self.message = ''
992    
993    
994     def _handleIHAVE(self, line):
995         if line == '.':
996             self.inputHandler = None
997             self.factory.backend.postRequest(
998                 self.message
999             ).addCallbacks(self._gotIHAVE, self._errIHAVE)
1000            
1001             self.message = ''
1002         else:
1003             self.message = self.message + line + '\r\n'
1004
1005
1006     def _gotIHAVE(self, result):
1007         self.sendLine('235 article transferred ok')
1008    
1009    
1010     def _errIHAVE(self, failure):
1011         print 'IHAVE failed: ', failure
1012         self.sendLine('436 transfer failed - try again later')
1013
1014
1015 class UsenetClientProtocol(NNTPClient):
1016     """
1017     A client that connects to an NNTP server and asks for articles new
1018     since a certain time.
1019     """
1020    
1021     def __init__(self, groups, date, storage):
1022         """
1023         Fetch all new articles from the given groups since the
1024         given date and dump them into the given storage.  groups
1025         is a list of group names.  date is an integer or floating
1026         point representing seconds since the epoch (GMT).  storage is
1027         any object that implements the NewsStorage interface.
1028         """
1029         NNTPClient.__init__(self)
1030         self.groups, self.date, self.storage = groups, date, storage
1031
1032
1033     def connectionMade(self):
1034         NNTPClient.connectionMade(self)
1035         log.msg("Initiating update with remote host: " + str(self.transport.getPeer()))
1036         self.setStream()
1037         self.fetchNewNews(self.groups, self.date, '')
1038
1039
1040     def articleExists(self, exists, article):
1041         if exists:
1042             self.fetchArticle(article)
1043         else:
1044             self.count = self.count - 1
1045             self.disregard = self.disregard + 1
1046
1047
1048     def gotNewNews(self, news):
1049         self.disregard = 0
1050         self.count = len(news)
1051         log.msg("Transfering " + str(self.count) + " articles from remote host: " + str(self.transport.getPeer()))
1052         for i in news:
1053             self.storage.articleExistsRequest(i).addCallback(self.articleExists, i)
1054
1055
1056     def getNewNewsFailed(self, reason):
1057         log.msg("Updated failed (" + reason + ") with remote host: " + str(self.transport.getPeer()))
1058         self.quit()
1059
1060
1061     def gotArticle(self, article):
1062         self.storage.postRequest(article)
1063         self.count = self.count - 1
1064         if not self.count:
1065             log.msg("Completed update with remote host: " + str(self.transport.getPeer()))
1066             if self.disregard:
1067                 log.msg("Disregarded %d articles." % (self.disregard,))
1068             self.factory.updateChecks(self.transport.getPeer())
1069             self.quit()
Note: See TracBrowser for help on using the browser.