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