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,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