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,840 @@
|
|
1
|
+
module Net
|
2
|
+
|
3
|
+
class NNTP
|
4
|
+
# NNTP Request base clase. Do not instantiate, use subclasses.
|
5
|
+
#
|
6
|
+
# == Class Hierarchy
|
7
|
+
#
|
8
|
+
# * Net::NNTP::Request
|
9
|
+
# * Net::NNTP::Authinfo
|
10
|
+
# * Net::NNTP::Capabilities
|
11
|
+
# * Net::NNTP::Date
|
12
|
+
# * Net::NNTP::Group
|
13
|
+
# * Net::NNTP::Hdr
|
14
|
+
# * Net::NNTP::Help
|
15
|
+
# * Net::NNTP::List
|
16
|
+
# * Net::NNTP::Modereader
|
17
|
+
# * Net::NNTP::Newgroups
|
18
|
+
# * Net::NNTP::Newnews
|
19
|
+
# * Net::NNTP::Over
|
20
|
+
# * Net::NNTP::PostingRequest
|
21
|
+
# * Net::NNTP::Post
|
22
|
+
# * Net::NNTP::Ihave
|
23
|
+
# * Net::NNTP::Quit
|
24
|
+
# * Net::NNTP::RetrievalRequest
|
25
|
+
# * Net::NNTP::Article
|
26
|
+
# * Net::NNTP::Body
|
27
|
+
# * Net::NNTP::Head
|
28
|
+
# * Net::NNTP::Stat
|
29
|
+
# * Net::NNTP::SelectionRequest
|
30
|
+
# * Net::NNTP::Group
|
31
|
+
# * Net::NNTP::Listgroup
|
32
|
+
# * Net::NNTP::Last
|
33
|
+
# * Net::NNTP::Next
|
34
|
+
# * Net::NNTP::Xhdr
|
35
|
+
# * Net::NNTP::Xover
|
36
|
+
# * Net::NNTP::Xpat
|
37
|
+
class Request
|
38
|
+
|
39
|
+
def initialize(command, rest=nil)
|
40
|
+
@command = ("%s %s" % [command.upcase, rest]).strip
|
41
|
+
end
|
42
|
+
|
43
|
+
def command
|
44
|
+
return [@command, "\r\n"].join
|
45
|
+
end
|
46
|
+
|
47
|
+
def dotstuff # :nodoc:
|
48
|
+
# dot-stuffing. Mail SHOULD be raw
|
49
|
+
stuff_regex = /^\./m
|
50
|
+
@body.to_s.gsub(stuff_regex, "..")
|
51
|
+
end
|
52
|
+
|
53
|
+
def msgid_or_range(rest) # :nodoc:
|
54
|
+
rval = ''
|
55
|
+
if rest
|
56
|
+
case rest
|
57
|
+
when String
|
58
|
+
rval = rest
|
59
|
+
when Hash
|
60
|
+
if rest.has_key? :msgid
|
61
|
+
rval = rest[:msgid].to_s
|
62
|
+
else
|
63
|
+
rval = range(rest)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
rval
|
68
|
+
end
|
69
|
+
|
70
|
+
def range(rest) # :nodoc:
|
71
|
+
return rest unless rest
|
72
|
+
rval = ''
|
73
|
+
if rest.has_key? :start
|
74
|
+
rval = rest[:start].to_s << "-"
|
75
|
+
if rest.has_key? :end
|
76
|
+
rval << rest[:end].to_s
|
77
|
+
end
|
78
|
+
end
|
79
|
+
rval
|
80
|
+
end
|
81
|
+
|
82
|
+
def valid_responses
|
83
|
+
[]
|
84
|
+
end
|
85
|
+
private :valid_responses
|
86
|
+
|
87
|
+
# Can be used to make sure that the response is valid (or a generic response) according to RFCs 3977 and 4643
|
88
|
+
def valid_response?(response)
|
89
|
+
response.generic? || valid_responses().include?(response.class)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns the capability that the server should mention on a Capability Request to notify the user that the server
|
93
|
+
# is capable to process the request. Older and broken NNTP servers do not respond to a Capability request, thus
|
94
|
+
# leaving it to trial-and-error.
|
95
|
+
def capability
|
96
|
+
'mandatory'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class ParameterError < StandardError; end
|
101
|
+
|
102
|
+
# {RFC 4643}[http://tools.ietf.org/html/rfc4643]
|
103
|
+
#
|
104
|
+
# Plain text AUTHINFO Request. Set user and pass with new.
|
105
|
+
#
|
106
|
+
# Parameter:
|
107
|
+
#
|
108
|
+
# * keyword:: either 'user' or 'pass'
|
109
|
+
# * data:: username if keyword is USER, password if keyword is PASS
|
110
|
+
#
|
111
|
+
# == Usage Example
|
112
|
+
#
|
113
|
+
# nntp = Net::NNTP.new
|
114
|
+
# nntp.server = 'localhost'
|
115
|
+
# nntp.connect
|
116
|
+
# request = Authinfo.new('user', 'test')
|
117
|
+
# response = nntp.process(request)
|
118
|
+
# if PasswordRequired === response
|
119
|
+
# request = Authinfo.new('pass', 'xxx')
|
120
|
+
# response = nntp.process(request)
|
121
|
+
# if OKResponse === response
|
122
|
+
# # authenticated, go ahead
|
123
|
+
# else
|
124
|
+
# raise StandardError, response.message
|
125
|
+
# end
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# Valid Responses: PasswordRequired, AuthenticationAccepted, AuthenticationFailed, AuthenticationOutOfSequence
|
129
|
+
class Authinfo < Request
|
130
|
+
|
131
|
+
# +user+ is the string used in AUTHINFO USER
|
132
|
+
# +pass+ is the string used in AUTHINFO PASS
|
133
|
+
#
|
134
|
+
# See: user_command, pass_command
|
135
|
+
def initialize(keyword, data)
|
136
|
+
super 'AUTHINFO', [keyword.upcase, data].join(" ")
|
137
|
+
end
|
138
|
+
|
139
|
+
def valid_responses
|
140
|
+
[PasswordRequired, AuthenticationAccepted, AuthenticationFailed, AuthenticationOutOfSequence]
|
141
|
+
end
|
142
|
+
private :valid_responses
|
143
|
+
|
144
|
+
def capability
|
145
|
+
'authinfo'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Classification of Requests expecting response codes in the 1xx range.
|
150
|
+
#
|
151
|
+
# == Subclasses:
|
152
|
+
# * Help
|
153
|
+
# * Capabilities
|
154
|
+
class InformationRequest < Request
|
155
|
+
class << self
|
156
|
+
private :new
|
157
|
+
end
|
158
|
+
end
|
159
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
160
|
+
#
|
161
|
+
# Valid Response: HelpResponse
|
162
|
+
class Help < InformationRequest
|
163
|
+
class << self
|
164
|
+
public :new
|
165
|
+
end
|
166
|
+
def initialize
|
167
|
+
super 'HELP'
|
168
|
+
end
|
169
|
+
def valid_responses
|
170
|
+
[HelpResponse]
|
171
|
+
end
|
172
|
+
private :valid_responses
|
173
|
+
end
|
174
|
+
|
175
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
176
|
+
#
|
177
|
+
# CAPABILITIES Request. Should be used to discover the commands that the server can process. Can change if MODE or
|
178
|
+
# authentication changes the state of the connection. Sadly, most servers do not implement this, leaving either the
|
179
|
+
# option to parse the informal response to a HELP request, or use trial-and-error.
|
180
|
+
#
|
181
|
+
# == Usage Example
|
182
|
+
#
|
183
|
+
# nntp = Net::NNTP.new
|
184
|
+
# nntp.server = 'localhost'
|
185
|
+
# nntp.connect
|
186
|
+
# request = Capabilities.new
|
187
|
+
# response = nntp.process(request)
|
188
|
+
# response.capabilities['version'] # returns the version, either as number, or array of numbers, i.e. [2, 3]
|
189
|
+
#
|
190
|
+
# Valid Response: CapabilityList
|
191
|
+
class Capabilities < InformationRequest
|
192
|
+
class << self
|
193
|
+
public :new
|
194
|
+
end
|
195
|
+
def initialize
|
196
|
+
super 'CAPABILITIES'
|
197
|
+
end
|
198
|
+
|
199
|
+
def valid_responses
|
200
|
+
[CapabilityList]
|
201
|
+
end
|
202
|
+
private :valid_responses
|
203
|
+
end
|
204
|
+
|
205
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
206
|
+
#
|
207
|
+
# Generates a 'MODE READER' command.
|
208
|
+
#
|
209
|
+
# Valid Responses: PostingAllowed, PostingProhibited
|
210
|
+
class Modereader < Request
|
211
|
+
def initialize()
|
212
|
+
super 'MODE', 'READER'
|
213
|
+
end
|
214
|
+
|
215
|
+
def valid_responses
|
216
|
+
[PostingAllowed, PostingProhibited]
|
217
|
+
end
|
218
|
+
private :valid_responses
|
219
|
+
|
220
|
+
def capability
|
221
|
+
'mode-reader'
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
226
|
+
#
|
227
|
+
# Generates a QUIT command. *Important*: after issuing QUIT to the server, the connection MUST be closed by the
|
228
|
+
# client after a ConnectionClosing Response has been received.
|
229
|
+
#
|
230
|
+
# == Usage Example
|
231
|
+
#
|
232
|
+
# nntp = Net::NNTP.new
|
233
|
+
# nntp.server = 'localhost'
|
234
|
+
# nntp.connect
|
235
|
+
# request = Quit.new
|
236
|
+
# response = nntp.process(request)
|
237
|
+
# if (ConnectionClosing === response)
|
238
|
+
# nntp.disconnect
|
239
|
+
# end
|
240
|
+
#
|
241
|
+
# Valid Response: ConnectionClosing
|
242
|
+
class Quit < Request
|
243
|
+
def initialize
|
244
|
+
super 'QUIT'
|
245
|
+
end
|
246
|
+
def valid_responses
|
247
|
+
[ConnectionClosing]
|
248
|
+
end
|
249
|
+
private :valid_responses
|
250
|
+
end
|
251
|
+
|
252
|
+
# Categorizes requests that manipulate the internal server state (selecting groups and/or articles)
|
253
|
+
#
|
254
|
+
# Parent: Net::NNTP::Request
|
255
|
+
#
|
256
|
+
# Subclasses:
|
257
|
+
#
|
258
|
+
# * Net::NNTP::Group
|
259
|
+
# * Net::NNTP::Listgroup
|
260
|
+
# * Net::NNTP::Last
|
261
|
+
# * Net::NNTP::Next
|
262
|
+
class SelectionRequest < Request
|
263
|
+
class << self
|
264
|
+
private :new
|
265
|
+
end
|
266
|
+
|
267
|
+
def capability
|
268
|
+
'reader'
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
273
|
+
#
|
274
|
+
# Request for retrieving group information.
|
275
|
+
#
|
276
|
+
# Parent: Net::NNTP::SelectionRequest
|
277
|
+
#
|
278
|
+
# Valid Response: GroupSelected
|
279
|
+
class Group < SelectionRequest
|
280
|
+
class << self
|
281
|
+
public :new
|
282
|
+
end
|
283
|
+
|
284
|
+
#
|
285
|
+
# +group+ is the group name for which the information should be
|
286
|
+
# retrieved.
|
287
|
+
def initialize(groupname)
|
288
|
+
super 'GROUP', groupname
|
289
|
+
end
|
290
|
+
|
291
|
+
def valid_responses
|
292
|
+
[GroupSelected]
|
293
|
+
end
|
294
|
+
private :valid_responses
|
295
|
+
end
|
296
|
+
|
297
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
298
|
+
#
|
299
|
+
# LISTGROUP request.
|
300
|
+
#
|
301
|
+
# Parent: Net::NNTP::SelectionRequest
|
302
|
+
#
|
303
|
+
# Valid Responses: GroupSelected, GroupUnknown, NoGroupSelected
|
304
|
+
class Listgroup < SelectionRequest
|
305
|
+
class << self
|
306
|
+
public :new
|
307
|
+
end
|
308
|
+
|
309
|
+
# * +groupname+ is the group that should be selected. If a name is given, the group will be selected; on success,
|
310
|
+
# the first article returned will be selected. If no name is given, the currently selected group will be used,
|
311
|
+
# and no range can be specified.
|
312
|
+
# * +range+ a range of article numbers in the specified group, given as Hash with the keys :start and :end; if
|
313
|
+
# :end is not specified, the range is seen as 'open range', thus selecting all articles beginning with the
|
314
|
+
# number given as :start value to the last number available in the group.
|
315
|
+
#
|
316
|
+
# == Usage Example
|
317
|
+
#
|
318
|
+
# # no groupname
|
319
|
+
# request = Net::NNTP::Listgroup.new
|
320
|
+
# # request.command => 'LISTGROUP'
|
321
|
+
# # only groupname
|
322
|
+
# request = Net::NNTP::Listgroup.new('alt.test')
|
323
|
+
# # request.command => 'LISTGROUP alt.test'
|
324
|
+
# # given only a start number
|
325
|
+
# request = Net::NNTP::Listgroup.new('alt.test', :start => 1000)
|
326
|
+
# # request.command => 'LISTGROUP alt.test 1000-'
|
327
|
+
#
|
328
|
+
# # given a range
|
329
|
+
# request = Net::NNTP::Listgroup.new('alt.test', :start => 1000, :end => 5000)
|
330
|
+
# # request.command => 'LISTGROUP alt.test 1000- 5000'
|
331
|
+
def initialize(groupname=nil, range={})
|
332
|
+
param = nil
|
333
|
+
if groupname then
|
334
|
+
param = groupname
|
335
|
+
unless range.empty?
|
336
|
+
if range.has_key?(:start)
|
337
|
+
if range.has_key?(:end)
|
338
|
+
param = "#{groupname} #{range[:start]}-#{range[:end]}"
|
339
|
+
else
|
340
|
+
param = "#{groupname} #{range[:start]}-"
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
super 'LISTGROUP', param
|
346
|
+
end
|
347
|
+
|
348
|
+
def valid_responses
|
349
|
+
[GroupSelected, GroupUnknown, NoGroupSelected]
|
350
|
+
end
|
351
|
+
private :valid_responses
|
352
|
+
end
|
353
|
+
|
354
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
355
|
+
#
|
356
|
+
# Request to select the article preceding the selected article in the current group.
|
357
|
+
#
|
358
|
+
# Parent: Net::NNTP::SelectionRequest
|
359
|
+
#
|
360
|
+
# Valid Responses: ArticleSelected, NoGroupSelected, InvalidArticle, NoPreviousArticle
|
361
|
+
class Last < SelectionRequest
|
362
|
+
class << self
|
363
|
+
public :new
|
364
|
+
end
|
365
|
+
|
366
|
+
def initialize
|
367
|
+
super 'LAST'
|
368
|
+
end
|
369
|
+
|
370
|
+
def valid_responses
|
371
|
+
[ArticleSelected, NoGroupSelected, InvalidArticle, NoPreviousArticle]
|
372
|
+
end
|
373
|
+
private :valid_responses
|
374
|
+
end
|
375
|
+
|
376
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
377
|
+
#
|
378
|
+
# LAST request.
|
379
|
+
#
|
380
|
+
# Parent: Net::NNTP::SelectionRequest
|
381
|
+
#
|
382
|
+
# Valid Responses: ArticleSelected, NoGroupSelected, InvalidArticle, NoNextArticle
|
383
|
+
class Next < SelectionRequest
|
384
|
+
class << self
|
385
|
+
public :new
|
386
|
+
end
|
387
|
+
|
388
|
+
def initialize
|
389
|
+
super 'NEXT'
|
390
|
+
end
|
391
|
+
|
392
|
+
def valid_responses
|
393
|
+
[ArticleSelected, NoGroupSelected, InvalidArticle, NoNextArticle]
|
394
|
+
end
|
395
|
+
private :valid_responses
|
396
|
+
end
|
397
|
+
|
398
|
+
# Categorizes the requests which are used to retrieve article information.
|
399
|
+
#
|
400
|
+
# Parent: Net::Request
|
401
|
+
#
|
402
|
+
# Subclasses:
|
403
|
+
#
|
404
|
+
# * Net::NNTP::Article
|
405
|
+
# * Net::NNTP::Head
|
406
|
+
# * Net::NNTP::Body
|
407
|
+
# * Net::NNTP::Stat
|
408
|
+
class RetrievalRequest < Request
|
409
|
+
class << self
|
410
|
+
private :new
|
411
|
+
end
|
412
|
+
def initialize(keyword, param)
|
413
|
+
super keyword, param
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
418
|
+
#
|
419
|
+
# ARTICLE Request (headers and body).
|
420
|
+
#
|
421
|
+
# Valid Responses: ArticleResponse, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid
|
422
|
+
class Article < RetrievalRequest
|
423
|
+
class << self
|
424
|
+
public :new
|
425
|
+
end
|
426
|
+
|
427
|
+
#
|
428
|
+
# * +param+ is either a message-id or an article number in the selected group.
|
429
|
+
#
|
430
|
+
# == Usage Example:
|
431
|
+
# request = Net::NNTP::Article.new(100) # => request.command will be 'ARTICLE 100'
|
432
|
+
def initialize(param=nil)
|
433
|
+
super 'ARTICLE', param
|
434
|
+
end
|
435
|
+
def valid_responses
|
436
|
+
[ArticleResponse, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid]
|
437
|
+
end
|
438
|
+
private :valid_responses
|
439
|
+
|
440
|
+
def capability
|
441
|
+
'reader'
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
446
|
+
#
|
447
|
+
# HEAD request
|
448
|
+
#
|
449
|
+
# Valid Responses: HeaderResponse, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid
|
450
|
+
class Head < RetrievalRequest
|
451
|
+
class << self
|
452
|
+
public :new
|
453
|
+
end
|
454
|
+
#
|
455
|
+
# * +param+ is either a message-id or an article number in the selected group.
|
456
|
+
#
|
457
|
+
# see Net::NNTP::Article.new
|
458
|
+
def initialize(param=nil)
|
459
|
+
super 'HEAD', param
|
460
|
+
end
|
461
|
+
def valid_responses
|
462
|
+
[HeaderResponse, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid]
|
463
|
+
end
|
464
|
+
private :valid_responses
|
465
|
+
end
|
466
|
+
|
467
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
468
|
+
#
|
469
|
+
# BODY request
|
470
|
+
#
|
471
|
+
# Valid Responses: BodyResponse, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid
|
472
|
+
class Body < RetrievalRequest
|
473
|
+
class << self
|
474
|
+
public :new
|
475
|
+
end
|
476
|
+
#
|
477
|
+
# * +param+ is either a message-id or an article number in the selected group.
|
478
|
+
#
|
479
|
+
# see Net::NNTP::Article.new
|
480
|
+
def initialize(param=nil)
|
481
|
+
super 'BODY', param
|
482
|
+
end
|
483
|
+
def valid_responses
|
484
|
+
[BodyResponse, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid]
|
485
|
+
end
|
486
|
+
private :valid_responses
|
487
|
+
|
488
|
+
def capability
|
489
|
+
'reader'
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
494
|
+
#
|
495
|
+
# STAT request
|
496
|
+
#
|
497
|
+
# Valid Responses: ArticleSelected, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid
|
498
|
+
class Stat < Request
|
499
|
+
class << self
|
500
|
+
public :new
|
501
|
+
end
|
502
|
+
#
|
503
|
+
# * +param+ is either a message-id or an article number in the selected group.
|
504
|
+
#
|
505
|
+
# see Net::NNTP::Article.new
|
506
|
+
def initialize(param=nil)
|
507
|
+
super 'STAT', param
|
508
|
+
end
|
509
|
+
|
510
|
+
def valid_responses
|
511
|
+
[ArticleSelected, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid]
|
512
|
+
end
|
513
|
+
private :valid_responses
|
514
|
+
end
|
515
|
+
|
516
|
+
# Posting Request Base class.
|
517
|
+
#
|
518
|
+
# Do NOT instantiate, use subclasses Post or Ihave.
|
519
|
+
class PostingRequest < Request
|
520
|
+
class << self
|
521
|
+
private :new
|
522
|
+
end
|
523
|
+
|
524
|
+
def initialize(keyword, param=nil)
|
525
|
+
super keyword, param
|
526
|
+
end
|
527
|
+
|
528
|
+
# Sets the body of the posting to be transmitted.
|
529
|
+
#
|
530
|
+
# Do NOT use any dot stuffing or <CRLF>.<CRLF> sequences - dot-stuffing will happen automatically when sending.
|
531
|
+
#
|
532
|
+
# +body+ can be a TMail::Mail object, a string or any line-based IO object supporting +readline+, where the
|
533
|
+
# content has to be a valid mail, able to be parsed. The *body* attribute will be set to a TMail::Mail object,
|
534
|
+
# ready to be sent.
|
535
|
+
#
|
536
|
+
# == Example
|
537
|
+
# # ...
|
538
|
+
# request = Post.new # use subclass Post
|
539
|
+
# request.body =<<-EOF
|
540
|
+
# Newsgroups: alt.test
|
541
|
+
# From: Demo User <user@example.com>
|
542
|
+
# Subject: I am a test posting
|
543
|
+
#
|
544
|
+
# This is just a test posting
|
545
|
+
# EOF
|
546
|
+
# puts request.body.to_s
|
547
|
+
#
|
548
|
+
# # => From: Demo User <user@example.com>
|
549
|
+
# # => Subject: I am a test posting
|
550
|
+
# # => Newsgroups: alt.test
|
551
|
+
# # =>
|
552
|
+
# # => This is just a test posting.
|
553
|
+
#
|
554
|
+
def body=(body)
|
555
|
+
if body.is_a?(TMail::Mail)
|
556
|
+
@body = body
|
557
|
+
else
|
558
|
+
if body.respond_to? :readline
|
559
|
+
begin
|
560
|
+
raw = ''
|
561
|
+
loop do
|
562
|
+
line = body.readline
|
563
|
+
raw << line
|
564
|
+
end
|
565
|
+
rescue EOFError => e
|
566
|
+
end
|
567
|
+
else
|
568
|
+
raw = body
|
569
|
+
end
|
570
|
+
@body = TMail::Mail.parse(raw)
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
# Returns the *body* attribute; given a block, sets the body from the result of that block first.
|
575
|
+
def body
|
576
|
+
if block_given?
|
577
|
+
self.body = yield
|
578
|
+
end
|
579
|
+
@body
|
580
|
+
end
|
581
|
+
|
582
|
+
end
|
583
|
+
|
584
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
585
|
+
#
|
586
|
+
# POST request.
|
587
|
+
#
|
588
|
+
# Valid Responses: ArticleReceived, PostArticle, PostingNotPermitted, PostingFailed
|
589
|
+
class Post < PostingRequest
|
590
|
+
class << self
|
591
|
+
public :new
|
592
|
+
end
|
593
|
+
|
594
|
+
# The parameter +body+ should be the body in Mail format. DO NOT use dot-stuffing or <CR-LF>.<CR-LF> delimiter!
|
595
|
+
#
|
596
|
+
# See PostingRequest for an example.
|
597
|
+
def initialize(body=nil)
|
598
|
+
self.body = body unless body.nil?
|
599
|
+
super "POST"
|
600
|
+
end
|
601
|
+
|
602
|
+
def valid_responses
|
603
|
+
[ArticleReceived, PostArticle, PostingNotPermitted, PostingFailed]
|
604
|
+
end
|
605
|
+
private :valid_responses
|
606
|
+
end
|
607
|
+
|
608
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
609
|
+
#
|
610
|
+
# IHAVE request.
|
611
|
+
#
|
612
|
+
# Valid Responses: TransferOK, TransferArticle, ArticleNotWanted, TransferNotPossible, TransferRejected
|
613
|
+
class Ihave < PostingRequest
|
614
|
+
class << self
|
615
|
+
public :new
|
616
|
+
end
|
617
|
+
|
618
|
+
# * +id+ is the messageid that will be presented to the server. Will be superficially formally checked
|
619
|
+
# * +body+ is the posting that should be subsequently posted on a good response (335). It MUST NOT
|
620
|
+
# be dot-stuffed.
|
621
|
+
# See also: Post, PostingRequest, TransferOK, TransferArticle, ArticleNotWanted, TransferNotPossible, TransferRejected
|
622
|
+
def initialize(id, body=nil)
|
623
|
+
raise ParameterError,"Messageid must not be nil!" unless id
|
624
|
+
raise ParameterError,"Malformed messageid" unless id.match(/<.+@.+>/)
|
625
|
+
self.body = body unless body.nil?
|
626
|
+
super "IHAVE", id
|
627
|
+
end
|
628
|
+
|
629
|
+
def valid_responses
|
630
|
+
[TransferOK, TransferArticle, ArticleNotWanted, TransferNotPossible, TransferRejected]
|
631
|
+
end
|
632
|
+
private :valid_responses
|
633
|
+
|
634
|
+
def capability
|
635
|
+
'ihave'
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
640
|
+
#
|
641
|
+
# DATE request.
|
642
|
+
#
|
643
|
+
# Valid Response: DateResponse
|
644
|
+
class Date < Request
|
645
|
+
def initialize
|
646
|
+
super 'DATE'
|
647
|
+
end
|
648
|
+
|
649
|
+
def valid_responses
|
650
|
+
[DateResponse]
|
651
|
+
end
|
652
|
+
private :valid_responses
|
653
|
+
|
654
|
+
def capability
|
655
|
+
'reader'
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
660
|
+
#
|
661
|
+
# NEWGROUPS request.
|
662
|
+
#
|
663
|
+
# Valid Response: NewgroupsResponse
|
664
|
+
class Newgroups < Request
|
665
|
+
# * +date_str+ should be a string that will be parsed by DateTime.parse. Do not use system dates, rather use the
|
666
|
+
# date given by a Date request or a formerly stored date from a Date request.
|
667
|
+
# * +is_gmt+ denotes that the given date should be used as GMT (UTC) date (adding the string GMT to the request.)
|
668
|
+
# Use if possible.
|
669
|
+
def initialize(date_str, is_gmt=true)
|
670
|
+
super 'NEWGROUPS', DateTime.parse(date_str, true).strftime('%Y%m%d %H%M%S') << (is_gmt ? ' GMT' : '')
|
671
|
+
end
|
672
|
+
|
673
|
+
def valid_responses
|
674
|
+
[NewgroupsResponse]
|
675
|
+
end
|
676
|
+
private :valid_responses
|
677
|
+
|
678
|
+
def capability
|
679
|
+
'reader'
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
684
|
+
#
|
685
|
+
# NEWNEWS request.
|
686
|
+
#
|
687
|
+
# Valid Response: NewnewsResponse
|
688
|
+
class Newnews < Request
|
689
|
+
#
|
690
|
+
# * +group+ must be an array of group wildmatches which should be checked for news since date_str.
|
691
|
+
# * +date_str+ should be a string that will be parsed by DateTime.parse. Do not use system dates, rather use the
|
692
|
+
# date given by a Date request or a formerly stored date from a Date request.
|
693
|
+
# * +is_gmt+ denotes that the given date should be used as GMT (UTC) date (adding the string GMT to the request.)
|
694
|
+
# Use if possible.
|
695
|
+
def initialize(groups, date_str, is_gmt=true)
|
696
|
+
super 'NEWNEWS', groups.join(',') << ' ' << DateTime.parse(date_str, true).strftime('%Y%m%d %H%M%S') << (is_gmt ? ' GMT' : '')
|
697
|
+
end
|
698
|
+
|
699
|
+
def valid_responses
|
700
|
+
[NewnewsResponse]
|
701
|
+
end
|
702
|
+
private :valid_responses
|
703
|
+
end
|
704
|
+
|
705
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
706
|
+
#
|
707
|
+
# LIST request.
|
708
|
+
#
|
709
|
+
# Valid Response: ListInformationFollows
|
710
|
+
class List < Request
|
711
|
+
attr_reader :keyword
|
712
|
+
# * +keyword+ must be nil or one of ACTIVE, ACTIVE.TIMES, DISTRIB.PATS, HEADERS, NEWSGROUPS or OVERVIEW.FMT.
|
713
|
+
# * +rest+ can be a string or a hash with either the key :msgid or the keys :start and optionally :end to denote a
|
714
|
+
# range.
|
715
|
+
#
|
716
|
+
# == Example
|
717
|
+
# request = Net::NNTP::List.new # => LIST
|
718
|
+
# request = Net::NNTP::List.new('ACTIVE') # => LIST ACTIVE
|
719
|
+
# request = Net::NNTP::List.new('ACTIVE.TIMES', 'at.*') # => LIST ACTIVE at.*
|
720
|
+
# request = Net::NNTP::List.new('HEADERS', :msgid => '<this.is@invalid>') # => LIST HEADERS <this.is@invalid>
|
721
|
+
# request = Net::NNTP::List.new('HEADERS', :start => 123, :end => 456) # => LIST HEADERS 123-456
|
722
|
+
#
|
723
|
+
def initialize(keyword=nil, rest={})
|
724
|
+
Net::NNTP.logger.debug("Keyword: <#{keyword}>")
|
725
|
+
@keyword = keyword.strip.upcase if keyword
|
726
|
+
parameters = (@keyword ? @keyword.dup : '')
|
727
|
+
parameters << ' ' << msgid_or_range(rest)
|
728
|
+
super 'LIST', parameters
|
729
|
+
end
|
730
|
+
|
731
|
+
def valid_responses
|
732
|
+
[ListInformationFollows]
|
733
|
+
end
|
734
|
+
|
735
|
+
def capability
|
736
|
+
'list'
|
737
|
+
end
|
738
|
+
end
|
739
|
+
|
740
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
741
|
+
#
|
742
|
+
# OVER request.
|
743
|
+
#
|
744
|
+
# Valid Responses: OverviewInformation, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid
|
745
|
+
class Over < Request
|
746
|
+
# * +range+ can be a string or a hash with either the key :msgid or the keys :start and optionally :end to denote a
|
747
|
+
# range.
|
748
|
+
def initialize(range=nil)
|
749
|
+
super 'OVER', msgid_or_range(range)
|
750
|
+
end
|
751
|
+
|
752
|
+
def valid_responses
|
753
|
+
[OverviewInformation, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid]
|
754
|
+
end
|
755
|
+
private :valid_responses
|
756
|
+
|
757
|
+
def capability
|
758
|
+
'over'
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
# {RFC 3977}[http://tools.ietf.org/html/rfc3977]
|
763
|
+
#
|
764
|
+
# HDR request.
|
765
|
+
#
|
766
|
+
# Valid Responses: HdrResponse, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid
|
767
|
+
class Hdr < Request
|
768
|
+
# * +field+ is the header field that should be retrieved
|
769
|
+
# * +range+ can be a string or a hash with either the key :msgid or the keys :start and optionally :end to denote a
|
770
|
+
# range.
|
771
|
+
def initialize(field, range=nil)
|
772
|
+
params = field
|
773
|
+
params << ' ' << msgid_or_range(range) if range
|
774
|
+
super 'HDR', params
|
775
|
+
end
|
776
|
+
|
777
|
+
def valid_responses
|
778
|
+
[HdrResponse, NoGroupSelected, InvalidArticle, InvalidNumberOrRange, NoSuchMessageid]
|
779
|
+
end
|
780
|
+
private :valid_responses
|
781
|
+
|
782
|
+
def capability
|
783
|
+
'hdr'
|
784
|
+
end
|
785
|
+
end
|
786
|
+
|
787
|
+
# Extension {RFC2980}[http://tools.ietf.org/html/rfc2980]
|
788
|
+
#
|
789
|
+
# XHDR request.
|
790
|
+
#
|
791
|
+
# *Warning*:: Deprecated in {RFC 3977}[http://tools.ietf.org/html/rfc3977]. See Hdr. Still there, because a lot of
|
792
|
+
# servers do run the old version.
|
793
|
+
#
|
794
|
+
# See also: HeaderResponse
|
795
|
+
class Xhdr < Request
|
796
|
+
#
|
797
|
+
# * +field+ is the header field that should be retrieved
|
798
|
+
# * +range+ can be a string or a hash with either the key :msgid or the keys :start and optionally :end to denote a
|
799
|
+
# range.
|
800
|
+
def initialize(field, range=nil)
|
801
|
+
params = field
|
802
|
+
params << ' ' << msgid_or_range(range) if range
|
803
|
+
super 'XHDR', params
|
804
|
+
end
|
805
|
+
end
|
806
|
+
|
807
|
+
# Extension {RFC2980}[http://tools.ietf.org/html/rfc2980]
|
808
|
+
#
|
809
|
+
# XOVER request.
|
810
|
+
#
|
811
|
+
# *Warning*:: Deprecated in {RFC 3977}[http://tools.ietf.org/html/rfc3977]. See Over. Still there, because a lot of
|
812
|
+
# servers do run the old version.
|
813
|
+
#
|
814
|
+
# Valid Response: OverviewInformation
|
815
|
+
class Xover < Request
|
816
|
+
#
|
817
|
+
# * +range+ can be a string or a hash with either the key :msgid or the keys :start and optionally :end to denote a
|
818
|
+
# range.
|
819
|
+
def initialize(range=nil)
|
820
|
+
super 'XOVER', range(range)
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
# Extension {RFC2980}[http://tools.ietf.org/html/rfc2980]
|
825
|
+
#
|
826
|
+
# XPAT request.
|
827
|
+
class Xpat < Request
|
828
|
+
# * +field+ is the header field that should be searched
|
829
|
+
# * +patterns+ is a string or an array of patterns
|
830
|
+
# * +range+ can be a string or a hash with either the key :msgid or the keys :start and optionally :end to denote a
|
831
|
+
# range.
|
832
|
+
def initialize(field, patterns, range)
|
833
|
+
patterns = [patterns] unless patterns.is_a?(Array)
|
834
|
+
parameters = field << ' ' << msgid_or_range(range) << ' ' << patterns.join(' ')
|
835
|
+
super 'XPAT', parameters
|
836
|
+
end
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
end
|