thoughtafter-nntp 1.0.0.2 → 1.0.0.3

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