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.
- data/CHANGELOG +145 -128
- data/CHANGELOG.debian +5 -0
- data/Manifest.txt +37 -0
- data/README.txt +69 -0
- data/Rakefile +28 -0
- data/build-stamp +0 -0
- data/lib/net/nntp.rb +261 -327
- data/lib/net/nntp/request.rb +840 -0
- data/lib/net/nntp/response.rb +731 -0
- data/lib/net/nntp/version.rb +2 -1
- data/script/console +12 -0
- data/spec/net/nntp/request_spec.rb +771 -0
- data/spec/net/nntp/response_spec.rb +994 -0
- data/spec/net/nntp_spec.rb +634 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/stories/all.rb +2 -0
- data/spec/stories/helper.rb +17 -0
- data/spec/stories/net/nntp.rb +7 -0
- data/spec/stories/net/nntp.story +12 -0
- data/spec/stories/steps/nntp.rb +18 -0
- data/tasks/ann.rake +81 -0
- data/tasks/bones.rake +21 -0
- data/tasks/gem.rake +126 -0
- data/tasks/git.rake +41 -0
- data/tasks/manifest.rake +49 -0
- data/tasks/mercurial.rake +6 -0
- data/tasks/notes.rake +28 -0
- data/tasks/pallet.rake +17 -0
- data/tasks/post_load.rake +39 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +57 -0
- data/tasks/setup.rb +268 -0
- data/tasks/spec.rake +55 -0
- data/tasks/svn.rake +48 -0
- data/tasks/test.rake +38 -0
- metadata +57 -33
- data/README +0 -46
- data/lib/net/nntp/article.rb +0 -188
- data/lib/net/nntp/group.rb +0 -83
- data/test/functional/test_nntp.rb +0 -288
- data/test/mock/mock_socket.rb +0 -359
- data/test/unit/test_nntp_article.rb +0 -98
- data/test/unit/test_nntp_group.rb +0 -60
@@ -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
|