ruby-net-nntp 0.2.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,731 @@
1
+ module Net
2
+ class NNTP
3
+
4
+ class GenericError < StandardError; end
5
+ class ProtocolError < RuntimeError; end
6
+ # Simple header class to parse headers received from LIST OVERVIEW.FMT
7
+ #
8
+ # See Net::NNTP::ListInformationFollows
9
+ class FormatHeader
10
+ attr_reader :name, :full
11
+ def initialize(line)
12
+ if match = line.match(/^(\S+):(full)?/m)
13
+ @name = match[1].downcase
14
+ @full = ('full' == match[2])
15
+ elsif match = line.match(/^:(\S+)$/m)
16
+ @name = match[1].downcase
17
+ @full = false
18
+ end
19
+ end
20
+ end
21
+
22
+ # Prepares the basic functionality for responses that have a body (directly following the status line).
23
+ #
24
+ # Every class that includes this can use strings or objects responding to +readline+ to retrieve the body.
25
+ module BodyBaseResponse
26
+ # Sets the body attribute.
27
+ #
28
+ # The parameter +body+ can be a string or an object responding to +readline+. Reading will be halted at a line
29
+ # with a single dot followed by <CRLF> or <LF>, or when an EOFError occurs. The result will be stored in the
30
+ # internal attribute @raw. Subsequently, parse_body will be called.
31
+ def body=(body)
32
+ if body.respond_to? :readline
33
+ begin
34
+ @raw = ''
35
+ loop do
36
+ line = body.readline
37
+ @raw << line
38
+ break if line =~/\A\.\r?\n\z/m
39
+ end
40
+ rescue EOFError => e
41
+ end
42
+ else
43
+ @raw = body
44
+ end
45
+ parse_body
46
+ end
47
+ # Returns the body attribute.
48
+ #
49
+ # If a block is given, the results will be sent to body= before returning the attribute.
50
+ def body
51
+ self.body = yield if block_given?
52
+ @body
53
+ end
54
+ # Is called from body= and should be used to set the @body attribute transforming the contents of the @raw
55
+ # attribute.
56
+ def parse_body
57
+ @body = @raw.dup
58
+ end
59
+ end
60
+
61
+ # Base response class. Parent of all responses. DO NOT instantiate directly unless you're ready to handle the nitty
62
+ # gritty yourself.
63
+ #
64
+ # Class Tree:
65
+ # * Net::NNTP::Response
66
+ # * InformationResponse
67
+ # * HelpResponse
68
+ # * CapabilityList
69
+ # * DateResponse
70
+ # * OKResponse
71
+ # * PostingAllowed
72
+ # * PostingProhibited
73
+ # * ConnectionClosing
74
+ # * GroupSelected
75
+ # * ListInformationFollows
76
+ # * ArticleResponse
77
+ # * HeaderResponse
78
+ # * BodyResponse
79
+ # * ArticleSelected
80
+ # * OverviewInformation
81
+ # * HdrResponse
82
+ # * NewnewsResponse
83
+ # * NewgroupsResponse
84
+ # * TransferOK
85
+ # * ArticleReceived
86
+ # * AuthenticationAccepted
87
+ class Response
88
+ attr_reader :code, :message
89
+ # * Parameter +code+ is the code returned from the server, should be a string representing the first three bytes
90
+ # (digits).
91
+ # * Parameter +message+ is the message following the code on the status line without the separating whitespace.
92
+ # * Parameter +generic+ gives the subclasses the opportunity to denote if the response is a generic response
93
+ # according to the RFC, and therefor valid following any request, or if the response is a response that MUST
94
+ # follow a certain request. See Request for validating responses.
95
+ # * Parameter +multiline+ gives the subclasses the opportunity to denote if the response is a multiline response
96
+ # according to RFC, and if a body is to be expected.
97
+ def initialize(request, code, message, generic=false, multiline=false)
98
+ @code = code
99
+ @message = message
100
+ @generic=generic
101
+ @multiline = multiline
102
+ @request = request
103
+ end
104
+
105
+ # Returns the value of the *generic* attribute, set by the generic parameter to new.
106
+ def generic?
107
+ @generic
108
+ end
109
+
110
+ # Returns the value of the *multiline* attribute, set by the multiline parameter to new.
111
+ def multiline?
112
+ @multiline
113
+ end
114
+
115
+ # Returns *true* if the response is a multiline response and has a body set.
116
+ def has_body?
117
+ multiline? && body
118
+ end
119
+
120
+ # Returns the body in subclasses; returns nil in Response.
121
+ def body
122
+ nil
123
+ end
124
+
125
+ def ==(other)
126
+ if other.is_a? Response
127
+ self.class == other.class && self.code == other.code && self.message == other.message && self.multiline? == other.multiline? && self.generic? == other.generic? && self.body == other.body
128
+ else
129
+ return false
130
+ end
131
+ end
132
+
133
+ def needs_article?
134
+ false
135
+ end
136
+
137
+ def force_close?
138
+ false
139
+ end
140
+ end
141
+
142
+ # Categorizes the responses starting with a code of 1xx as information responses.
143
+ #
144
+ # All InformationResponse subclasses do have a body, so BodyBaseResponse is included here.
145
+ #
146
+ # == Subclasses
147
+ # * HelpResponse
148
+ # * CapabilityList
149
+ # * DateResponse
150
+ class InformationResponse < Response
151
+ include BodyBaseResponse
152
+ end
153
+
154
+ # Code: 100
155
+ #
156
+ # Response to a Help request.
157
+ class HelpResponse < InformationResponse
158
+ attr_reader :raw
159
+ def initialize(request, code, message)
160
+ super request, code, message, false, true
161
+ end
162
+ end
163
+
164
+ # Code: 101
165
+ #
166
+ # Response to a Capabilities request.
167
+ #
168
+ class CapabilityList < InformationResponse
169
+ attr_reader :raw
170
+
171
+ def initialize(request, code, message)
172
+ @capabilities = {}
173
+ super request, code, message, false, true
174
+ end
175
+
176
+ # Returns the capabilities as a hash. Key is the first word (capability) downcased, value either a single string
177
+ # or a array of strings, or the value 'true'.
178
+ #
179
+ def capabilities
180
+ @capabilities.dup
181
+ end
182
+ def parse_body # :nodoc: internal use only
183
+ @raw.split(/\n/).each do |line|
184
+ break if line == '.'
185
+ key, *value = line.split(/\s+/)
186
+ value = (value.size <= 1 ? value[0]: value)
187
+ @capabilities[key.downcase]= value || true
188
+ end unless @raw.nil?
189
+ end
190
+ end
191
+
192
+ # Code: 111
193
+ #
194
+ # DATE response.
195
+ class DateResponse < InformationResponse
196
+ # Returns the date given as message from server.
197
+ def date
198
+ DateTime.parse(@message)
199
+ end
200
+ end
201
+
202
+ # Categorizes responses starting with a code of 2xx as OKResponses
203
+ # Codes: 2xx
204
+ #
205
+ # DO NOT instantiate, use the following
206
+ #
207
+ # == Subclasses
208
+ # * PostingAllowed
209
+ # * PostingProhibited
210
+ # * ConnectionClosing
211
+ # * GroupSelected
212
+ # * ListInformationFollows
213
+ # * ArticleResponse
214
+ # * HeaderResponse
215
+ # * BodyResponse
216
+ # * ArticleSelected
217
+ # * OverviewInformation
218
+ # * HdrResponse
219
+ # * NewnewsResponse
220
+ # * NewgroupsResponse
221
+ # * TransferOK
222
+ # * ArticleReceived
223
+ # * AuthenticationAccepted
224
+ class OKResponse < Response
225
+ end
226
+
227
+ # Code: 200
228
+ # Posting allowed response. See NNTP#connect, Modereader.
229
+ class PostingAllowed < OKResponse
230
+ end
231
+
232
+ # Code: 201
233
+ # Posting prohibited response. See NNTP#connect, Modereader, Post.
234
+ class PostingProhibited < OKResponse
235
+ end
236
+
237
+ # Code: 205
238
+ # Connection closing response. See NNTP#quit.
239
+ class ConnectionClosing < OKResponse
240
+ def force_close?
241
+ true
242
+ end
243
+ end
244
+
245
+ # Code: 211
246
+ #
247
+ # GroupSelected response.
248
+ #
249
+ # See Group, Listgroup.
250
+ class GroupSelected < OKResponse
251
+ attr_reader :group, :list
252
+ attr_reader :raw
253
+ include BodyBaseResponse
254
+ def initialize(request, code, message)
255
+ @group = OpenStruct.new
256
+ tokens = message.split
257
+ raise ProtocolError, "Malformed group message: #{message}" unless tokens.size >= 4
258
+ @group.number, @group.low, @group.high, @group.name = tokens[0].to_i, tokens[1].to_i, tokens[2].to_i, tokens[3]
259
+ @body = nil
260
+ super request, code, message, false, Listgroup === request
261
+ end
262
+ def parse_body
263
+ @body=@raw
264
+ @list = @raw.gsub(/\r?\n\.\r?\n/, '').split(/\r?\n/)
265
+ end
266
+
267
+ end
268
+
269
+ class GroupListResponse < OKResponse
270
+ include BodyBaseResponse
271
+ attr_reader :list, :raw
272
+
273
+ def initialize(request, code, message)
274
+ @list = []
275
+ super request, code, message, false, true
276
+ end
277
+
278
+ end
279
+
280
+ # Code: 215
281
+ class ListInformationFollows < GroupListResponse
282
+ def parse_body
283
+ case @request.keyword
284
+ when nil, 'ACTIVE'
285
+ @raw.split(/\r?\n/).each do |line|
286
+ break if line == '.'
287
+ name, low, high, status = line.split(/\s+/)
288
+ @list << OpenStruct.new(:name => name, :low => low.to_i, :high => high.to_i, :status => status.strip)
289
+ end unless @raw.nil?
290
+ when 'ACTIVE.TIMES'
291
+ @raw.split(/\r?\n/).each do |line|
292
+ break if line == '.'
293
+ name, epoch, creator = line.split(/\s+/)
294
+ @list << OpenStruct.new(:name => name, :epoch => epoch, :creator => creator)
295
+ end unless @raw.nil?
296
+ when 'DISTRIB.PATS'
297
+ @raw.split(/\r?\n/).each do |line|
298
+ break if line == '.'
299
+ priority, pattern, distribution = line.split(/:/)
300
+ @list << OpenStruct.new(:priority => priority.to_i, :pattern => pattern, :distribution => distribution)
301
+ end unless @raw.nil?
302
+ when 'HEADERS'
303
+ @list = @raw.gsub(/\r?\n\.\r?\n/, '').split(/\r?\n/) unless @raw.nil?
304
+ when 'NEWSGROUPS'
305
+ @raw.split(/\r?\n/).each do |line|
306
+ break if line == '.'
307
+ if match = line.match(/\A(\S+)\s+(.*)\z/)
308
+ name, desc = match.captures
309
+ @list << OpenStruct.new(:name => name, :desc => desc)
310
+ else
311
+ raise ProtocolError,'List format mismatch'
312
+ end
313
+ end unless @raw.nil?
314
+ when 'OVERVIEW.FMT'
315
+ @raw.split(/\r?\n/).each do |line|
316
+ break if line == '.'
317
+ @list << FormatHeader.new(line)
318
+ end unless @raw.nil?
319
+ end
320
+ end
321
+
322
+ end
323
+
324
+ class ArticleBaseResponse < OKResponse
325
+ attr_reader :number, :message_id, :raw
326
+ def initialize(request, code, message, generic=false, multiline=true)
327
+ number, id = message.split(/\s+/)
328
+ @number = number.to_i
329
+ @message_id = id
330
+ super request, code, message, generic, multiline
331
+ end
332
+ end
333
+
334
+ # Code: 220
335
+ class ArticleResponse < ArticleBaseResponse
336
+ include BodyBaseResponse
337
+ def parse_body
338
+ unless @article
339
+ r = @raw.dup
340
+ r.gsub!(/^\./,"")
341
+ @article = TMail::Mail.parse(r)
342
+ end
343
+ end
344
+ def article
345
+ @article.dup
346
+ end
347
+ def [](header)
348
+ @article[header].to_s
349
+ end
350
+ end
351
+
352
+ # Code: 221
353
+ #
354
+ # CAVEAT:: according to RFC 2980, an XHDR request will trigger a 221 response, but with a TOTALLY different format.
355
+ # THANKS, IDIOTS (fucking on a heap of barbed wire ...)
356
+ class HeaderResponse < ArticleBaseResponse
357
+ include BodyBaseResponse
358
+ attr_reader :list
359
+ def parse_body
360
+ unless @article
361
+ if Xhdr === @request
362
+ @xhdr = true
363
+ @list = {}
364
+ @raw.split(/\r?\n/).each do |line|
365
+ number, value = line.split(/\t/)
366
+ @list[number.to_i] = value
367
+ end
368
+ elsif Head === @request
369
+ @xhdr = false
370
+ begin
371
+ @article = TMail::Mail.new
372
+ @article.__send__(:parse_header, StringIO.new(@raw))
373
+ rescue TMail::SyntaxError
374
+ end
375
+ end
376
+ end
377
+ end
378
+
379
+ def [](header)
380
+ return @article[header].to_s if @article
381
+ nil
382
+ end
383
+ def header
384
+ return @article.header if @article
385
+ nil
386
+ end
387
+
388
+ end
389
+
390
+
391
+ # Code: 222
392
+ class BodyResponse < ArticleBaseResponse
393
+ include BodyBaseResponse
394
+ end
395
+
396
+ # Code: 223
397
+ class ArticleSelected < ArticleBaseResponse
398
+ def initialize(request, code, message)
399
+ super request, code, message, false, false
400
+ end
401
+ end
402
+
403
+ # Code: 224
404
+ class OverviewInformation < ArticleBaseResponse
405
+ include BodyBaseResponse
406
+ attr_reader :subject, :from, :date, :references, :bytes, :lines, :optional_headers
407
+ def parse_body
408
+ raw = @raw.gsub(/\r\n\.\r\n/, '')
409
+ number, subject, from, date, message_id, references, bytes, lines, *rest =
410
+ raw.strip.split(/\t/)
411
+ @number = number.to_i
412
+ @subject = subject
413
+ @from = from
414
+ @date = DateTime.parse(date)
415
+ @message_id = message_id
416
+ @references = references.split(/\s/)
417
+ @bytes = bytes.to_i
418
+ @lines = lines.to_i
419
+ @optional_headers = {}
420
+ if rest
421
+ rest.each do |line|
422
+ header, value = line.split(/:\s+/)
423
+ optional_headers[header.downcase] = value
424
+ end
425
+ end
426
+ end
427
+ end
428
+
429
+ # Code : 225
430
+ class HdrResponse < ArticleBaseResponse
431
+ include BodyBaseResponse
432
+ attr_reader :list
433
+ def parse_body
434
+ @list = {}
435
+ @raw.split(/\r?\n/).each do |line|
436
+ number, value = line.split(/\t/)
437
+ @list[number.to_i] = value
438
+ end
439
+ end
440
+ end
441
+
442
+ # Code: 230
443
+ class NewnewsResponse < ArticleBaseResponse
444
+ include BodyBaseResponse
445
+ attr_reader :list
446
+ def parse_body
447
+ @list = @raw.split(/\r?\n/)
448
+ end
449
+ end
450
+
451
+ # Code: 231
452
+ class NewgroupsResponse < GroupListResponse
453
+ def parse_body
454
+ @raw.split(/\r?\n/).each do |line|
455
+ break if line == '.'
456
+ name, low, high, status = line.split(/\s+/)
457
+ @list << OpenStruct.new(:name => name, :low => low.to_i, :high => high.to_i, :status => status.strip)
458
+ end unless @raw.nil?
459
+ end
460
+
461
+ end
462
+
463
+ # Code: 235
464
+ class TransferOK < OKResponse
465
+ end
466
+
467
+ # Code: 240
468
+ class ArticleReceived < OKResponse
469
+ end
470
+
471
+ # Code: 281
472
+ class AuthenticationAccepted < OKResponse
473
+ end
474
+
475
+ class ContinueResponse < Response
476
+ end
477
+
478
+ # Code: 335
479
+ class TransferArticle < ContinueResponse
480
+ def needs_article?
481
+ true
482
+ end
483
+ end
484
+
485
+ # Code: 340
486
+ class PostArticle < ContinueResponse
487
+ def needs_article?
488
+ true
489
+ end
490
+ end
491
+
492
+ # Code: 381
493
+ class PasswordRequired < ContinueResponse
494
+ end
495
+
496
+
497
+ # Codes: 4xx
498
+ # == Subclasses:
499
+ # * TemporarilyUnavailable (400)
500
+ # * InternalFault (403)
501
+ # * GroupUnknown (411)
502
+ # * NoGroupSelected (412)
503
+ # * InvalidArticle (420)
504
+ # * NoNextArticle (421)
505
+ # * NoPreviousArticle (422)
506
+ # * InvalidNumberOrRange (423)
507
+ # * NoSuchMessageid (430)
508
+ # * ArticleNotWanted (435)
509
+ # * TransferNotPossible (436)
510
+ # * TransferRejected (437)
511
+ # * PostingNotPermitted (440)
512
+ # * PostingFailed (441)
513
+ # * AuthenticationRequired (480)
514
+ # * AuthenticationFailed (481)
515
+ # * AuthenticationOutOfSequence (482)
516
+ # * PrivacyRequired (483)
517
+ class RetryResponse < Response
518
+ end
519
+
520
+ # Code: 400
521
+ class TemporarilyUnavailable < RetryResponse
522
+ def initialize(request, code, message)
523
+ super request, code, message, true
524
+ end
525
+ end
526
+
527
+ # Code: 403
528
+ class InternalFault < RetryResponse
529
+ def initialize(request, code, message)
530
+ super request, code, message, true
531
+ end
532
+ end
533
+
534
+ # Code: 411
535
+ class GroupUnknown < RetryResponse
536
+ end
537
+
538
+ # Code: 412
539
+ class NoGroupSelected < RetryResponse
540
+ end
541
+
542
+ # Code: 420
543
+ class InvalidArticle < RetryResponse
544
+ end
545
+
546
+ # Code: 421
547
+ class NoPreviousArticle < RetryResponse
548
+ end
549
+
550
+ # Code: 422
551
+ class NoNextArticle < RetryResponse
552
+ end
553
+
554
+ # Code: 423
555
+ class InvalidNumberOrRange < RetryResponse
556
+ end
557
+
558
+ # Code: 430
559
+ class NoSuchMessageid < RetryResponse
560
+ end
561
+
562
+ # Code: 435
563
+ class ArticleNotWanted < RetryResponse
564
+ end
565
+
566
+ # Code: 436
567
+ class TransferNotPossible < RetryResponse
568
+ end
569
+
570
+ # Code: 437
571
+ class TransferRejected < RetryResponse
572
+ end
573
+
574
+ # Code: 440
575
+ class PostingNotPermitted < RetryResponse
576
+ end
577
+
578
+ # Code: 441
579
+ class PostingFailed < RetryResponse
580
+ end
581
+
582
+ # Code: 480
583
+ class AuthenticationRequired < RetryResponse
584
+ def initialize(request, code, message)
585
+ super request, code, message, true
586
+ end
587
+ end
588
+
589
+ # Code: 481
590
+ class AuthenticationFailed < RetryResponse
591
+ end
592
+
593
+ # Code: 482
594
+ class AuthenticationOutOfSequence < RetryResponse
595
+ end
596
+
597
+ # Code: 483
598
+ class PrivacyRequired < RetryResponse
599
+ def initialize(request, code, message)
600
+ super request, code, message, true
601
+ end
602
+ end
603
+
604
+ # Codes: 5xx
605
+ # == Subclasses:
606
+ # * NotImplemented
607
+ # * SyntaxError
608
+ # * PermanentlyUnavailable
609
+ # * FeatureNotProvided
610
+ # * EncodingError
611
+ class FailResponse < Response
612
+ end
613
+
614
+ # Code: 500
615
+ class NotImplemented < FailResponse
616
+ def initialize(request, code, message)
617
+ super request, code, message, true
618
+ end
619
+ end
620
+
621
+ # Code: 501
622
+ class SyntaxError < FailResponse
623
+ def initialize(request, code, message)
624
+ super request, code, message, true
625
+ end
626
+ end
627
+
628
+ # Code: 502
629
+ class PermanentlyUnavailable < FailResponse
630
+ def initialize(request, code, message)
631
+ super request, code, message, true
632
+ end
633
+ end
634
+
635
+ # Code: 503
636
+ class FeatureNotProvided < FailResponse
637
+ def initialize(request, code, message)
638
+ super request, code, message, true
639
+ end
640
+ end
641
+
642
+ # Code: 500
643
+ class EncodingError < FailResponse
644
+ def initialize(request, code, message)
645
+ super request, code, message, true
646
+ end
647
+ end
648
+
649
+ class UnknownResponse < Response
650
+ end
651
+ RESPONSES = {
652
+ '100' => HelpResponse,
653
+ '101' => CapabilityList,
654
+ '111' => DateResponse,
655
+
656
+ '200' => PostingAllowed,
657
+ '201' => PostingProhibited,
658
+ '205' => ConnectionClosing,
659
+ '211' => GroupSelected,
660
+ '215' => ListInformationFollows,
661
+ '220' => ArticleResponse,
662
+ '221' => HeaderResponse,
663
+ '222' => BodyResponse,
664
+ '223' => ArticleSelected,
665
+ '224' => OverviewInformation,
666
+ '225' => HdrResponse,
667
+ '230' => NewnewsResponse,
668
+ '231' => NewgroupsResponse,
669
+ '235' => TransferOK,
670
+ '240' => ArticleReceived,
671
+ '281' => AuthenticationAccepted,
672
+
673
+ '335' => TransferArticle,
674
+ '340' => PostArticle,
675
+ '381' => PasswordRequired,
676
+
677
+ '400' => TemporarilyUnavailable,
678
+ '403' => InternalFault,
679
+ '411' => GroupUnknown,
680
+ '412' => NoGroupSelected,
681
+ '420' => InvalidArticle,
682
+ '421' => NoNextArticle,
683
+ '422' => NoPreviousArticle,
684
+ '423' => InvalidNumberOrRange,
685
+ '430' => NoSuchMessageid,
686
+ '435' => ArticleNotWanted,
687
+ '436' => TransferNotPossible,
688
+ '437' => TransferRejected,
689
+ '440' => PostingNotPermitted,
690
+ '441' => PostingFailed,
691
+ '480' => AuthenticationRequired,
692
+ '481' => AuthenticationFailed,
693
+ '482' => AuthenticationOutOfSequence,
694
+ '483' => PrivacyRequired,
695
+
696
+ '500' => NotImplemented,
697
+ '501' => SyntaxError,
698
+ '502' => PermanentlyUnavailable,
699
+ '503' => FeatureNotProvided,
700
+ '504' => EncodingError
701
+ }
702
+ CLASSES = {
703
+ '1' => InformationResponse,
704
+ '2' => OKResponse,
705
+ '3' => ContinueResponse,
706
+ '4' => RetryResponse,
707
+ '5' => FailResponse
708
+ }
709
+
710
+ class Response
711
+ attr_accessor :request
712
+ class << self
713
+ def create(request, statusline)
714
+ match = statusline.strip.match(/\A(\d{3})\s?(.*)\z/) or raise ProtocolError, "Unknown Response"
715
+ Net::NNTP.logger.debug("Response#create: request = #{request}")
716
+ klass = class_from_code(match[1])
717
+ Net::NNTP.logger.debug("Response#create: class = #{klass}")
718
+ this = klass.new(request, *match.captures)
719
+ end
720
+ def class_from_code(code)
721
+ Net::NNTP::RESPONSES[code] or
722
+ Net::NNTP::CLASSES[code[0,1]] or
723
+ Net::NNTP::UnknownResponse
724
+ end
725
+
726
+ end
727
+ end
728
+
729
+ end
730
+
731
+ end