ruby-net-nntp 0.2.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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