~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/protocols/nntp.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2004-06-21 22:01:11 UTC
  • mto: (2.2.3 sid)
  • mto: This revision was merged to the branch mainline in revision 3.
  • Revision ID: james.westby@ubuntu.com-20040621220111-vkf909euqnyrp3nr
Tags: upstream-1.3.0
ImportĀ upstreamĀ versionĀ 1.3.0

Show diffs side-by-side

added added

removed removed

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