nntp 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 @@
1
+ +��3����"3�:I�s����&z�&�C����%^�nJ�����ΚJ倢v����Vׁv5��F=�e�����HuV�|�W��g��v~7O�S�{�����j�1{���{g�N�X�C3���T������( �����=vtG�� HH��{$:�#P��M4\�;�V�Q��ą��"�J��K�ξ���N��#�����xE6.��>7aK �y0T��Q�}�i_~$�(�d���g�%��>
@@ -0,0 +1,28 @@
1
+ == 1.0.0 2007-12-21
2
+
3
+ * 1 enhancement
4
+ * Convert to RubyGem (Albert Vernon).
5
+
6
+ == 0.0.4 2006-01-03
7
+
8
+ * 1 enhancement
9
+ * Add new methods #io_body and #io_longcmd (Mark Triggs).
10
+ * 2 bug fixes
11
+ * Fix backward compatibility with ruby-1.8.2 (Mark Triggs).
12
+ * Identify a single dot (.) as an EOF marker (Mark Triggs).
13
+
14
+ == 0.0.3 2005-12-28
15
+
16
+ * 2 bug fixes
17
+ * Fix minor bugs in Makefile (Balwinder Singh Dheeman).
18
+ * Fix minor bugs in nntp.rb (Mark Triggs).
19
+
20
+ == 0.0.2 2005-05-15
21
+
22
+ * 1 enhancement
23
+ * Create .deb and .rpm packages (alien).
24
+
25
+ == 0.0.1 2005-05-01
26
+
27
+ * 1 enhancement
28
+ * Alpha code, stated development (Balwinder Singh Dheeman).
@@ -0,0 +1,21 @@
1
+ Net::NNTP Client Library
2
+ ========================
3
+
4
+ Copyright (C) 2005-2006 by the following:
5
+
6
+ If you have contributed to this project, you deserve to be on this list.
7
+ Contact us (see: AUTHORS) and we'll add you.
8
+
9
+ Balwinder S "bsd" Dheeman <bsd/AT?rubyforge.org>, Just a Lead Developer
10
+ Mark Triggs <mark@dishevelled.net>, An avid User & Contributor
11
+
12
+
13
+ You are free to distribute this software under Version 2.1 of the GNU
14
+ Lesser General Public License. On Debian systems, refer to
15
+ /usr/share/common-licenses/LGPL-2.1 for the complete text of the GNU
16
+ Lesser General Public License.
17
+
18
+ Certain components (as annotated in the source) are licensed under the
19
+ terms of the GNU General Public License. On Debian systems, the complete
20
+ text of the GNU General Public License can be found in
21
+ /usr/share/common-licenses/GPL file.
@@ -0,0 +1,19 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ config/hoe.rb
7
+ config/requirements.rb
8
+ lib/nntp.rb
9
+ lib/nntp/version.rb
10
+ nntp_rb.html.patch
11
+ script/destroy
12
+ script/generate
13
+ script/txt2html
14
+ setup.rb
15
+ tasks/deployment.rake
16
+ tasks/environment.rake
17
+ tasks/website.rake
18
+ test/test_helper.rb
19
+ test/test_nntp.rb
@@ -0,0 +1,9 @@
1
+ Net::NNTP Client Library
2
+
3
+ This is a pure Ruby source (single file, fully documented) code, provides
4
+ a minimum requisite functions for NNTP (Network News Transfer Protocol)
5
+ clients, per [RFC977] (http://www.ietf.org/rfc/rfc977.txt), extentions
6
+ [RFC2980] (http://www.ietf.org/rfc/rfc2980.txt) and authentication [DRAFT]
7
+ (http://www.ietf.org/internet-drafts/draft-ietf-nntpext-authinfo-07.txt).
8
+ .
9
+ WWW: http://nntp.rubyforge.org/
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
@@ -0,0 +1,71 @@
1
+ require 'nntp/version'
2
+
3
+ AUTHOR = ['Balwinder Singh Dheeman', 'Albert Vernon', 'Bob Schafer', 'Mark Triggs'] # can also be an array of Authors
4
+ EMAIL = 'aevernon@nospam@rubyforge.org'
5
+ DESCRIPTION = 'Net::NNTP client library for Network News Transfer Protocol'
6
+ GEM_NAME = 'nntp' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'nntp' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "unknown"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = Nntp::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'nntp documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.author = AUTHOR
52
+ p.description = DESCRIPTION
53
+ p.email = EMAIL
54
+ p.summary = DESCRIPTION
55
+ p.url = HOMEPATH
56
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
57
+ p.test_globs = ["test/**/test_*.rb"]
58
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
59
+
60
+ # == Optional
61
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
62
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
63
+
64
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
65
+
66
+ end
67
+
68
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
69
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
70
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
71
+ hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,17 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
16
+
17
+ require 'nntp'
@@ -0,0 +1,871 @@
1
+ # = nntp.rb
2
+ #
3
+ # NNTP Client Library
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation; either version 2.1 of the License, or (at
8
+ # your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13
+ # more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program; if not, write to the Free Software Foundation,
17
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
18
+ #
19
+ # See Net::NNTP for detailed documentation.
20
+ #
21
+ # ==Download
22
+ #
23
+ # (http://rubyforge.org/projects/nntp)
24
+ #
25
+ # == Copyright
26
+ #
27
+ # Copyright (C) 2004-2007 by Dr Balwinder Singh Dheeman. Distributed under
28
+ # the GNU GPL (http://www.gnu.org/licenses/gpl.html). See the files "COPYING"
29
+ # and, or "Copyright" , supplied with all distributions for additional
30
+ # information.
31
+ #
32
+ # == Authors
33
+ #
34
+ # Balwinder Singh Dheeman <bsd.SANSPAM@rubyforge.org> (http://cto.homelinux.net/~bsd)
35
+ # Albert Vernon <aevernon.SANSPAM@rubyforge.org>
36
+ # Bob Schafer <rschafer.SANSPAM@rubyforge.org>
37
+ # Mark Triggs <mark.SANSPAM@dishevelled.net>
38
+
39
+ require 'net/protocol'
40
+ require 'digest/md5'
41
+ module Net #:nodoc:
42
+
43
+ # Module mixed in to all NNTP error classes
44
+ module NNTPError
45
+ # This *class* is module for some reason.
46
+ # In ruby 1.9.x, this module becomes a class.
47
+ end
48
+
49
+ # Represents an NNTP authentication error.
50
+ class NNTPAuthenticationError < ProtoAuthError
51
+ include NNTPError
52
+ end
53
+
54
+ # Represents NNTP error code 420 or 450, a temporary error.
55
+ class NNTPServerBusy < ProtoServerError
56
+ include NNTPError
57
+ end
58
+
59
+ # Represents NNTP error code 440, posting not permitted.
60
+ class NNTPPostingNotAllowed < ProtoServerError
61
+ include NNTPError
62
+ end
63
+
64
+ # Represents an NNTP command syntax error (error code 500)
65
+ class NNTPSyntaxError < ProtoSyntaxError
66
+ include NNTPError
67
+ end
68
+
69
+ # Represents a fatal NNTP error (error code 5xx, except for 500)
70
+ class NNTPFatalError < ProtoFatalError
71
+ include NNTPError
72
+ end
73
+
74
+ # Unexpected reply code returned from server.
75
+ class NNTPUnknownError < ProtoUnknownError
76
+ include NNTPError
77
+ end
78
+
79
+ # Error in NNTP response data.
80
+ class NNTPDataError
81
+ include NNTPError
82
+ end
83
+
84
+ # = Net::NNTP
85
+ #
86
+ # == What is This Library?
87
+ #
88
+ # This library provides functionality to retrieve and, or post Usenet news
89
+ # articles via NNTP, the Network News Transfer Protocol. The Usenet is a
90
+ # world-wide distributed discussion system. It consists of a set of
91
+ # "newsgroups" with names that are classified hierarchically by topic.
92
+ # "articles" or "messages" are "posted" to these newsgroups by people on
93
+ # computers with the appropriate software -- these articles are then
94
+ # broadcast to other interconnected NNTP servers via a wide variety of
95
+ # networks. For details of NNTP itself, see [RFC977]
96
+ # (http://www.ietf.org/rfc/rfc977.txt).
97
+ #
98
+ # == What is This Library NOT?
99
+ #
100
+ # This library does NOT provide functions to compose Usenet news. You
101
+ # must create and, or format them yourself as per guidelines per
102
+ # Standard for Interchange of Usenet messages, see [RFC850], [RFC2047]
103
+ # and a fews other RFC's (http://www.ietf.org/rfc/rfc850.txt),
104
+ # (http://www.ietf.org/rfc/rfc2047.txt).
105
+ #
106
+ # FYI: the official documentation on Usenet news extentions is: [RFC2980]
107
+ # (http://www.ietf.org/rfc/rfc2980.txt).
108
+ #
109
+ # == Examples
110
+ #
111
+ # === Posting Messages
112
+ #
113
+ # You must open a connection to an NNTP server before posting messages.
114
+ # The first argument is the address of your NNTP server, and the second
115
+ # argument is the port number. Using NNTP.start with a block is the simplest
116
+ # way to do this. This way, the NNTP connection is closed automatically
117
+ # after the block is executed.
118
+ #
119
+ # require 'rubygems'
120
+ # require 'nntp'
121
+ # Net::NNTP.start('your.nntp.server', 119) do |nntp|
122
+ # # Use the NNTP object nntp only in this block.
123
+ # end
124
+ #
125
+ # Replace 'your.nntp.server' with your NNTP server. Normally your system
126
+ # manager or internet provider supplies a server for you.
127
+ #
128
+ # Then you can post messages.
129
+ #
130
+ # require 'date'
131
+ # date = DateTime.now().strftime(fmt='%a, %d %b %Y %T %z')
132
+ #
133
+ # msgstr = <<END_OF_MESSAGE
134
+ # From: Your Name <your@mail.address>
135
+ # Newsgroups: news.group.one, news.group.two ...
136
+ # Subject: test message
137
+ # Date: #{date}
138
+ #
139
+ # This is a test message.
140
+ # END_OF_MESSAGE
141
+ #
142
+ # require 'rubygems'
143
+ # require 'nntp'
144
+ # Net::NNTP.start('your.nntp.server', 119) do |nntp|
145
+ # nntp.post msgstr
146
+ # end
147
+ #
148
+ # *NOTE*: The NNTP message headers such as +Date:+, +Message-ID:+ and, or
149
+ # +Path:+ if ommited, may also be generated and added by your Usenet news
150
+ # server; better you verify the behavior of your news server.
151
+ #
152
+ # === Closing the Session
153
+ #
154
+ # You MUST close the NNTP session after posting messages, by calling the
155
+ # Net::NNTP#finish method:
156
+ #
157
+ # # using NNTP#finish
158
+ # nntp = Net::NNTP.start('your.nntp.server', 119)
159
+ # nntp.post msgstr
160
+ # nntp.finish
161
+ #
162
+ # You can also use the block form of NNTP.start/NNTP#start. This closes
163
+ # the NNTP session automatically:
164
+ #
165
+ # # using block form of NNTP.start
166
+ # Net::NNTP.start('your.nntp.server', 119) do |nntp|
167
+ # nntp.post msgstr
168
+ # end
169
+ #
170
+ # I strongly recommend this scheme. This form is simpler and more robust.
171
+ #
172
+ # === NNTP Authentication
173
+ #
174
+ # The Net::NNTP class may support various authentication schemes depending
175
+ # on your news server's reponse to CAPABILITIES command. To use NNTP
176
+ # authentication, pass extra arguments to NNTP.start/NNTP#start.
177
+ #
178
+ # See NNTP Extension for Authentication:
179
+ # (http://www.ietf.org/internet-drafts/draft-ietf-nntpext-authinfo-07.txt)
180
+ #
181
+ # Net::NNTP.start('your.nntp.server', 119,
182
+ # 'YourAccountName', 'YourPassword', :method)
183
+ #
184
+ # Where +:method+ can be one of the 'gassapi', 'digest_md5',
185
+ # 'cram_md5', 'starttls', 'external', 'plain', 'generic', 'simple' or
186
+ # 'original'; the later and, or unencrypted ones are less secure!
187
+ #
188
+ # In the case of method +:generic+ argumnents should be passed to a format
189
+ # string as follows:
190
+ #
191
+ # Net::NNTP.start('your.nntp.server', 119,
192
+ # "format", *arguments, :generic)
193
+ #
194
+ # *NOTE*: The Authentication mechanism will fallback to a lesser secure
195
+ # scheme, if your Usenet server does not supports method opted by you,
196
+ # except for the +:generic+ option.
197
+ #
198
+ class NNTP
199
+
200
+ # The default NNTP port, port 119.
201
+ def NNTP.default_port
202
+ 119
203
+ end
204
+
205
+ # Creates a new Net::NNTP object.
206
+ #
207
+ # +address+ is the hostname or ip address of your NNTP server. +port+ is
208
+ # the port to connect to; it defaults to port 119.
209
+ #
210
+ # This method does not opens any TCP connection. You can use NNTP.start
211
+ # instead of NNTP.new if you want to do everything at once. Otherwise,
212
+ # follow NNTP.new with optional changes to +:open_timeout+,
213
+ # +:read_timeout+ and, or +NNTP#set_debug_output+ and then NNTP#start.
214
+ #
215
+ def initialize(address, port = nil)
216
+ @address = address
217
+ @port = (port || NNTP.default_port)
218
+ @socket = nil
219
+ @started = false
220
+ @open_timeout = 30
221
+ @read_timeout = 60
222
+ @error_occured = false
223
+ @debug_output = nil
224
+ end
225
+
226
+ # Provide human-readable stringification of class state.
227
+ def inspect #:nodoc:
228
+ "#<#{self.class} #{@address}:#{@port} started=#{@started}>"
229
+ end
230
+
231
+ # The address of the NNTP server to connect to.
232
+ attr_reader :address
233
+
234
+ # The port number of the NNTP server to connect to.
235
+ attr_reader :port
236
+
237
+ # Seconds to wait while attempting to open a connection. If the
238
+ # connection cannot be opened within this time, a TimeoutError is raised.
239
+ attr_accessor :open_timeout
240
+
241
+ # Seconds to wait while reading one block (by one read(2) call). If the
242
+ # read(2) call does not complete within this time, a TimeoutError is
243
+ # raised.
244
+ attr_reader :read_timeout
245
+
246
+ # Set the number of seconds to wait until timing-out a read(2) call.
247
+ def read_timeout=(sec)
248
+ @socket.read_timeout = sec if @socket
249
+ @read_timeout = sec
250
+ end
251
+
252
+ # Set an output stream for debug logging. You must call this before
253
+ # #start.
254
+ #
255
+ # === Example
256
+ #
257
+ # nntp = Net::NNTP.new(addr, port)
258
+ # nntp.set_debug_output $stderr
259
+ # nntp.start do |nntp|
260
+ # ....
261
+ # end
262
+ #
263
+ # *WARNING*: This method causes serious security holes. Use this method
264
+ # for only debugging.
265
+ #
266
+ def set_debug_output(arg)
267
+ @debug_output = arg
268
+ end
269
+
270
+ #
271
+ # NNTP session control
272
+ #
273
+
274
+ # Creates a new Net::NNTP object and connects to the server.
275
+ #
276
+ # This method is equivalent to:
277
+ #
278
+ # Net::NNTP.new(address, port).start(account, password, :method)
279
+ #
280
+ # === Example
281
+ #
282
+ # Net::NNTP.start('your.nntp.server') do |nntp|
283
+ # nntp.post msgstr
284
+ # end
285
+ #
286
+ # === Block Usage
287
+ #
288
+ # If called with a block, the newly-opened Net::NNTP object is yielded to
289
+ # the block, and automatically closed when the block finishes. If called
290
+ # without a block, the newly-opened Net::NNTP object is returned to the
291
+ # caller, and it is the caller's responsibility to close it when
292
+ # finished.
293
+ #
294
+ # === Parameters
295
+ #
296
+ # +address+ is the hostname or ip address of your nntp server.
297
+ #
298
+ # +port+ is the port to connect to; it defaults to port 119.
299
+ #
300
+ # The remaining arguments are used for NNTP authentication, if required
301
+ # or desired. +user+ is the account name, +secret+ is your password or
302
+ # other authentication token, and +method+ is the authentication
303
+ # type; defaults to 'original'. Please read the discussion of NNTP
304
+ # Authentication in the overview notes above.
305
+ #
306
+ # === Errors
307
+ #
308
+ # This method may raise:
309
+ #
310
+ # * Net::NNTPAuthenticationError
311
+ # * Net::NNTPFatalError
312
+ # * Net::NNTPServerBusy
313
+ # * Net::NNTPSyntaxError
314
+ # * Net::NNTPUnknownError
315
+ # * IOError
316
+ # * TimeoutError
317
+ #
318
+ def NNTP.start(address, port = nil,
319
+ user = nil, secret = nil, method = nil,
320
+ &block) # :yield: nntp
321
+ new(address, port).start(user, secret, method, &block)
322
+ end
323
+
324
+ # +true+ if the NNTP session has been started.
325
+ def started?
326
+ @started
327
+ end
328
+
329
+ # Opens a TCP connection and starts the NNTP session.
330
+ #
331
+ # === Parameters
332
+ #
333
+ # If both of +user+ and +secret+ are given, NNTP authentication will be
334
+ # attempted using the AUTH command. The +method+ specifies the type of
335
+ # authentication to attempt; it must be one of :original, :simple,
336
+ # :generic, :plain, :starttls, :external, :cram_md5, :digest_md5 and, or
337
+ # :gassapi may be used. See the discussion of NNTP Authentication in the
338
+ # overview notes.
339
+ #
340
+ #
341
+ # === Block Usage
342
+ #
343
+ # When this methods is called with a block, the newly-started NNTP object
344
+ # is yielded to the block, and automatically closed after the block call
345
+ # finishes. Otherwise, it is the caller's responsibility to close the
346
+ # session when finished.
347
+ #
348
+ # === Example
349
+ #
350
+ # This is very similar to the class method NNTP.start.
351
+ #
352
+ # require 'rubygems'
353
+ # require 'nntp'
354
+ # nntp = Net::NNTP.new('nntp.news.server', 119)
355
+ # nntp.start(account, password, method) do |nntp|
356
+ # nntp.post msgstr
357
+ # end
358
+ #
359
+ # The primary use of this method (as opposed to NNTP.start) is probably
360
+ # to set debugging (#set_debug_output), which must be done before the
361
+ # session is started.
362
+ #
363
+ # === Errors
364
+ #
365
+ # If session has already been started, an IOError will be raised.
366
+ #
367
+ # This method may raise:
368
+ #
369
+ # * Net::NNTPAuthenticationError
370
+ # * Net::NNTPFatalError
371
+ # * Net::NNTPServerBusy
372
+ # * Net::NNTPSyntaxError
373
+ # * Net::NNTPUnknownError
374
+ # * IOError
375
+ # * TimeoutError
376
+ #
377
+ def start(user = nil, secret = nil, method = nil) # :yield: nntp
378
+ if block_given?
379
+ begin
380
+ do_start(user, secret, method)
381
+ return yield(self)
382
+ ensure
383
+ do_finish
384
+ end
385
+ else
386
+ do_start(user, secret, method)
387
+ return self
388
+ end
389
+ end
390
+
391
+ def do_start(user, secret, method) #:nodoc:
392
+ raise IOError, 'NNTP session already started' if @started
393
+ check_auth_args user, secret, method if user or secret
394
+
395
+ if InternetMessageIO.respond_to?(:old_open)
396
+ @socket = InternetMessageIO.old_open(@address, @port, @open_timeout,
397
+ @read_timeout, @debug_output)
398
+
399
+ else
400
+ @socket = InternetMessageIO.open(@address, @port, @open_timeout,
401
+ @read_timeout, @debug_output)
402
+ end
403
+
404
+ check_response(critical { recv_response() })
405
+
406
+ mode_reader_success = false
407
+ tried_authenticating = false
408
+ until mode_reader_success
409
+ begin
410
+ mode_reader
411
+ mode_reader_success = true
412
+ rescue NNTPAuthenticationError
413
+ if tried_authenticating
414
+ raise
415
+ end
416
+ rescue ProtocolError
417
+ raise
418
+ end
419
+ authenticate user, secret, method if user
420
+ tried_authenticating = true
421
+ end
422
+
423
+ authenticate user, secret, method if user
424
+ @started = true
425
+ ensure
426
+ @socket.close if not @started and @socket and not @socket.closed?
427
+ end
428
+ private :do_start
429
+
430
+ # Finishes the NNTP session and closes TCP connection. Raises IOError if
431
+ # not started.
432
+ def finish
433
+ raise IOError, 'not yet started' unless started?
434
+ do_finish
435
+ end
436
+
437
+ def do_finish #:nodoc:
438
+ quit if @socket and not @socket.closed? and not @error_occured
439
+ ensure
440
+ @started = false
441
+ @error_occured = false
442
+ @socket.close if @socket and not @socket.closed?
443
+ @socket = nil
444
+ end
445
+ private :do_finish
446
+
447
+ public
448
+
449
+ # POST
450
+ #
451
+ # Posts +msgstr+ as a message. Single CR ("\r") and LF ("\n") found in
452
+ # the +msgstr+, are converted into the CR LF pair. You cannot post a
453
+ # binary message with this method. +msgstr+ _should include both the
454
+ # message headers and body_. All non US-ASCII, binary and, or multi-part
455
+ # messages should be submitted in an encoded form as per MIME standards.
456
+ #
457
+ # === Example
458
+ #
459
+ # Net::NNTP.start('nntp.example.com') do |nntp|
460
+ # nntp.post msgstr
461
+ # end
462
+ #
463
+ # === Errors
464
+ #
465
+ # This method may raise:
466
+ #
467
+ # * Net::NNTPFatalError
468
+ # * Net::NNTPPostingNotAllowed
469
+ # * Net::NNTPServerBusy
470
+ # * Net::NNTPSyntaxError
471
+ # * Net::NNTPUnknownError
472
+ # * IOError
473
+ # * TimeoutError
474
+ #
475
+ def post(msgstr)
476
+ stat = post0 {
477
+ @socket.write_message msgstr
478
+ }
479
+ return stat[0..3], stat[4..-1].chop
480
+ end
481
+
482
+ # Opens a message writer stream and gives it to the block. The stream is
483
+ # valid only in the block, and has these methods:
484
+ #
485
+ # puts(str = ''):: outputs STR and CR LF.
486
+ # print(str):: outputs STR.
487
+ # printf(fmt, *args):: outputs sprintf(fmt,*args).
488
+ # write(str):: outputs STR and returns the length of written bytes.
489
+ # <<(str):: outputs STR and returns self.
490
+ #
491
+ # If a single CR ("\r") or LF ("\n") is found in the message, it is
492
+ # converted to the CR LF pair. You cannot post a binary message with
493
+ # this method.
494
+ #
495
+ # === Parameters
496
+ #
497
+ # Block
498
+ #
499
+ # === Example
500
+ #
501
+ # Net::NNTP.start('nntp.example.com', 119) do |nntp|
502
+ # nntp.open_message_stream do |f|
503
+ # f.puts 'From: from@example.com'
504
+ # f.puts 'Newsgroups: news.group.one, news.group.two ...'
505
+ # f.puts 'Subject: test message'
506
+ # f.puts
507
+ # f.puts 'This is a test message.'
508
+ # end
509
+ # end
510
+ #
511
+ # === Errors
512
+ #
513
+ # This method may raise:
514
+ #
515
+ # * Net::NNTPFatalError
516
+ # * Net::NNTPPostingNotAllowed
517
+ # * Net::NNTPServerBusy
518
+ # * Net::NNTPSyntaxError
519
+ # * Net::NNTPUnknownError
520
+ # * IOError
521
+ # * TimeoutError
522
+ #
523
+ def open_message_stream(&block) # :yield: stream
524
+ post0 { @socket.write_message_by_block(&block) }
525
+ end
526
+
527
+ # ARTICLE [<Message-ID>|<Number>]
528
+ def article(id_num = nil)
529
+ stat, text = longcmd("ARTICLE #{id_num}".strip)
530
+ return stat[0..2], text
531
+ end
532
+
533
+ # BODY [<Message-ID>|<Number>]
534
+ def body(id_num = nil)
535
+ stat, text = longcmd("BODY #{id_num}".strip)
536
+ return stat[0..2], text
537
+ end
538
+
539
+ # IO_BODY <output IO object> [<Message-ID>|<Number>]
540
+ def io_body (io_output, id_num = nil)
541
+ stat = io_longcmd(io_output, "BODY #{id_num}".strip)
542
+ return stat[0..2], io_output
543
+ end
544
+
545
+ # DATE
546
+ def date
547
+ text = []
548
+ stat = shortcmd("DATE")
549
+ text << stat[4...12]
550
+ text << stat[12...18]
551
+ raise NNTPDataError, stat, caller unless text[0].length == 8 and text[1].length == 6
552
+ return stat[0..2], text
553
+ end
554
+
555
+ # GROUP <Newsgroup>
556
+ def group(ng)
557
+ stat = shortcmd("GROUP %s", ng)
558
+ return stat[0..2], stat[4..-1].chop
559
+ end
560
+
561
+ # HEAD [<Message-ID>|<Number>]
562
+ def head(id_num = nil)
563
+ stat, text = longcmd("HEAD #{id_num}".strip)
564
+ return stat[0..2], text
565
+ end
566
+
567
+ # HELP
568
+ def help
569
+ stat, text = longcmd('HELP')
570
+ text.each_with_index do |line, index|
571
+ text[index] = line.gsub(/\A\s+/, '')
572
+ end
573
+ return stat[0..2], text
574
+ end
575
+
576
+ # LAST
577
+ def last
578
+ stat = shortcmd('LAST')
579
+ return stat[0..2], stat[4..-1].chop
580
+ end
581
+
582
+ # LIST [ACTIVE|NEWSGROUPS] [<Wildmat>]]:br:
583
+ # LIST [ACTIVE.TIMES|EXTENSIONS|SUBSCRIPTIONS|OVERVIEW.FMT]
584
+ def list(opts = nil)
585
+ stat, text = longcmd("LIST #{opts}".strip)
586
+ return stat[0..2], text
587
+ end
588
+
589
+ # LISTGROUP <Newsgroup>
590
+ def listgroup(ng)
591
+ stat, text = longcmd("LISTGROUP #{ng}".strip)
592
+ return stat[0..2], text
593
+ end
594
+
595
+ # MODE READER
596
+ def mode_reader
597
+ stat = shortcmd('MODE READER')
598
+ return stat[0..2], stat[4..-1].chop
599
+ end
600
+ private :mode_reader #:nodoc:
601
+
602
+ # NEWGROUPS <yymmdd> <hhmmss> [GMT]
603
+ def newgroups(date, time, tzone = nil)
604
+ stat, text = longcmd("NEWGROUPS #{date} #{time} #{tzone}".strip)
605
+ return stat[0..2], text
606
+ end
607
+
608
+ # NEXT
609
+ def next
610
+ stat = shortcmd('NEXT')
611
+ return stat[0..2], stat[4..-1].chop
612
+ end
613
+
614
+ # OVER <Range> # e.g first[-[last]]
615
+ def over(range)
616
+ stat, text = longcmd("OVER #{range}".strip)
617
+ return stat[0..2], text
618
+ end
619
+
620
+ # QUIT
621
+ def quit
622
+ stat = shortcmd('QUIT')
623
+ end
624
+ private :quit #:nodoc:
625
+
626
+ # SLAVE
627
+ def slave
628
+ stat = shortcmd('SLAVE')
629
+ return stat[0..2], stat[4..-1].chop
630
+ end
631
+
632
+ # STAT [<Message-ID>|<Number>]
633
+ def stat(id_num = nil)
634
+ stat = shortcmd("STAT #{id_num}".strip)
635
+ return stat[0..2], stat[4..-1].chop
636
+ end
637
+
638
+ # XHDR <Header> <Message-ID>|<Range> # e.g first[-[last]]
639
+ def xhdr(header, id_range)
640
+ stat, text = longcmd("XHDR #{header} #{id_range}".strip)
641
+ return stat[0..2], text
642
+ end
643
+
644
+ # XOVER <Range> # e.g first[-[last]]
645
+ def xover(range)
646
+ stat, text = longcmd("XOVER #{range}".strip)
647
+ return stat[0..2], text
648
+ end
649
+
650
+ private
651
+
652
+ #
653
+ # row level library
654
+ #
655
+
656
+ def post0
657
+ raise IOError, 'closed session' unless @socket
658
+ stat = critical {
659
+ check_response(get_response('POST'), true)
660
+ yield
661
+ recv_response()
662
+ }
663
+ check_response(stat)
664
+ end
665
+
666
+ #
667
+ # auth
668
+ #
669
+
670
+ def check_auth_args(user, secret, method)
671
+ raise ArgumentError, 'both user and secret are required'\
672
+ unless user and secret
673
+ authmeth = "auth_#{method || 'original'}"
674
+ raise ArgumentError, "wrong auth type #{method}"\
675
+ unless respond_to?(authmeth, true)
676
+ end
677
+
678
+ def authenticate(user, secret, method)
679
+ methods = %w(original simple generic plain starttls external cram_md5 digest_md5 gassapi)
680
+ method = "#{method || 'original'}"
681
+ authmeth = methods.index(method)
682
+ begin
683
+ __send__("auth_#{method}", user, secret)
684
+ rescue NNTPAuthenticationError
685
+ if authmeth and authmeth > 0
686
+ authmeth -= 1 # fallback
687
+ method = methods[authmeth]
688
+ @error_occured = false
689
+ retry
690
+ else
691
+ raise
692
+ end
693
+ end
694
+ end
695
+
696
+ # AUTHINFO USER username
697
+ # AUTHINFO PASS password
698
+ def auth_original(user, secret)
699
+ stat = critical {
700
+ check_response(get_response("AUTHINFO USER %s", user), true)
701
+ check_response(get_response("AUTHINFO PASS %s", secret), true)
702
+ }
703
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
704
+ end
705
+
706
+ # AUTHINFO SIMPLE
707
+ # username password
708
+ def auth_simple(user, secret)
709
+ stat = critical {
710
+ check_response(get_response('AUTHINFO SIMPLE'), true)
711
+ check_response(get_response('%s %s', user, secret), true)
712
+ }
713
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
714
+ end
715
+
716
+ # AUTHINFO GENERIC authenticator arguments ...
717
+ #
718
+ # The authentication protocols are not inculeded in RFC2980,
719
+ # see [RFC1731] (http://www.ieft.org/rfc/rfc1731.txt).
720
+ def auth_generic(fmt, *args)
721
+ stat = critical {
722
+ cmd = 'AUTHINFO GENERIC ' + sprintf(fmt, *args)
723
+ check_response(get_response(cmd), true)
724
+ }
725
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
726
+ end
727
+
728
+ # AUTHINFO SASL PLAIN
729
+ def auth_plain(user, secret)
730
+ stat = critical {
731
+ check_response(get_response('AUTHINFO SASL PLAIN %s',
732
+ base64_encode("\0#{user}\0#{secret}")), true)
733
+ }
734
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
735
+ end
736
+
737
+ # STARTTLS
738
+ def auth_starttls(user, secret)
739
+ stat = critical {
740
+ check_response(get_response('STARTTLS'), true)
741
+ ### FIXME:
742
+ }
743
+ raise NNTPAuthenticationError, 'not implemented as yet!'
744
+ end
745
+
746
+ # AUTHINFO SASL EXTERNAL =
747
+ def auth_external(user, secret)
748
+ stat = critical {
749
+ check_response(get_response('AUTHINFO SASL EXTERNAL ='), true)
750
+ ### FIXME:
751
+ }
752
+ raise NNTPAuthenticationError, 'not implemented as yet!'
753
+ end
754
+
755
+ # AUTHINFO SASL CRAM-MD5 [RFC2195]
756
+ def auth_cram_md5(user, secret)
757
+ stat = nil
758
+ critical {
759
+ stat = check_response(get_response('AUTHINFO SASL CRAM-MD5'), true)
760
+ challenge = stat.split(/ /)[1].unpack('m')[0]
761
+ secret = Digest::MD5.digest(secret) if secret.size > 64
762
+
763
+ isecret = secret + "\0" * (64 - secret.size)
764
+ osecret = isecret.dup
765
+ 0.upto(63) do |i|
766
+ isecret[i] ^= 0x36
767
+ osecret[i] ^= 0x5c
768
+ end
769
+ tmp = Digest::MD5.digest(isecret + challenge)
770
+ tmp = Digest::MD5.hexdigest(osecret + tmp)
771
+
772
+ stat = get_response(base64_encode(user + ' ' + tmp))
773
+ }
774
+ raise NNTPAuthenticationError, stat unless /\A2../ === stat
775
+ end
776
+
777
+ # AUTHINFO SASL DIGEST-MD5
778
+ def auth_digest_md5(user, secret)
779
+ stat = critical {
780
+ check_response(get_response('AUTHINFO SASL DIGEST-MD5'), true)
781
+ ### FIXME:
782
+ }
783
+ raise NNTPAuthenticationError, 'not implemented as yet!'
784
+ end
785
+
786
+ # AUTHINFO SASL GASSAPI
787
+ def auth_gassapi(user, secret)
788
+ stat = critical {
789
+ check_response(get_response('AUTHINFO SASL GASSAPI'), true)
790
+ ### FIXME:
791
+ }
792
+ raise NNTPAuthenticationError, 'not implemented as yet!'
793
+ end
794
+
795
+ def base64_encode(str)
796
+ # expects "str" may not become too long
797
+ [str].pack('m').gsub(/\s+/, '')
798
+ end
799
+
800
+ def longcmd(fmt, *args)
801
+ text = []
802
+ stat = io_longcmd(text, fmt, *args)
803
+ return stat, text.map { |line| line.chomp! }
804
+ end
805
+
806
+ def io_longcmd(target, fmt, *args)
807
+ if stat = shortcmd(fmt, *args)
808
+ while true
809
+ line = @socket.readline
810
+ break if line =~ /^\.\s*$/ # done
811
+ line = line[1..-1] if line.to_s[0...2] == '..'
812
+ target << line + $/
813
+ end
814
+ end
815
+
816
+ return stat, target
817
+ end
818
+
819
+ def shortcmd(fmt, *args)
820
+ stat = critical {
821
+ @socket.writeline sprintf(fmt, *args)
822
+ recv_response()
823
+ }
824
+ check_response(stat)
825
+ end
826
+
827
+ def get_response(fmt, *args)
828
+ @socket.writeline sprintf(fmt, *args)
829
+ recv_response()
830
+ end
831
+
832
+ def recv_response
833
+ stat = ''
834
+ while true
835
+ line = @socket.readline
836
+ stat << line << "\n"
837
+ break unless line[3] == ?- # "210-PIPELINING"
838
+ end
839
+ stat
840
+ end
841
+
842
+ def check_response(stat, allow_continue = false)
843
+ return stat if /\A1/ === stat # 1xx info msg
844
+ return stat if /\A2/ === stat # 2xx cmd k
845
+ return stat if allow_continue and /\A[35]/ === stat # 3xx cmd k, snd rst
846
+ exception = case stat
847
+ when /\A440/ then NNTPPostingNotAllowed # 4xx cmd k, bt nt prfmd
848
+ when /\A48/ then NNTPAuthenticationError
849
+ when /\A4/ then NNTPServerBusy
850
+ when /\A50/ then NNTPSyntaxError # 5xx cmd ncrrct
851
+ when /\A55/ then NNTPFatalError
852
+ else
853
+ NNTPUnknownError
854
+ end
855
+ raise exception, stat
856
+ end
857
+
858
+ def critical(&block)
859
+ return '200 dummy reply code' if @error_occured
860
+ begin
861
+ return yield()
862
+ rescue Exception
863
+ @error_occured = true
864
+ raise
865
+ end
866
+ end
867
+
868
+ end # class_NNTP
869
+
870
+ NNTPSession = NNTP
871
+ end # module_Net