~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/news/nntp.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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()