entp-astrotrain 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/.gitignore +26 -0
  2. data/LICENSE +20 -0
  3. data/README +47 -0
  4. data/Rakefile +145 -0
  5. data/VERSION +1 -0
  6. data/astrotrain.gemspec +96 -0
  7. data/config/sample.rb +12 -0
  8. data/lib/astrotrain/api.rb +53 -0
  9. data/lib/astrotrain/logged_mail.rb +41 -0
  10. data/lib/astrotrain/mapping/http_post.rb +18 -0
  11. data/lib/astrotrain/mapping/jabber.rb +23 -0
  12. data/lib/astrotrain/mapping/transport.rb +55 -0
  13. data/lib/astrotrain/mapping.rb +157 -0
  14. data/lib/astrotrain/message.rb +313 -0
  15. data/lib/astrotrain/tmail.rb +48 -0
  16. data/lib/astrotrain.rb +55 -0
  17. data/lib/vendor/rest-client/README.rdoc +104 -0
  18. data/lib/vendor/rest-client/Rakefile +84 -0
  19. data/lib/vendor/rest-client/bin/restclient +65 -0
  20. data/lib/vendor/rest-client/foo.diff +66 -0
  21. data/lib/vendor/rest-client/lib/rest_client/net_http_ext.rb +21 -0
  22. data/lib/vendor/rest-client/lib/rest_client/payload.rb +185 -0
  23. data/lib/vendor/rest-client/lib/rest_client/request_errors.rb +75 -0
  24. data/lib/vendor/rest-client/lib/rest_client/resource.rb +103 -0
  25. data/lib/vendor/rest-client/lib/rest_client.rb +189 -0
  26. data/lib/vendor/rest-client/rest-client.gemspec +18 -0
  27. data/lib/vendor/rest-client/spec/base.rb +5 -0
  28. data/lib/vendor/rest-client/spec/master_shake.jpg +0 -0
  29. data/lib/vendor/rest-client/spec/payload_spec.rb +71 -0
  30. data/lib/vendor/rest-client/spec/request_errors_spec.rb +44 -0
  31. data/lib/vendor/rest-client/spec/resource_spec.rb +52 -0
  32. data/lib/vendor/rest-client/spec/rest_client_spec.rb +219 -0
  33. data/tasks/doc.thor +149 -0
  34. data/tasks/merb.thor +2020 -0
  35. data/test/api_test.rb +28 -0
  36. data/test/fixtures/apple_multipart.txt +100 -0
  37. data/test/fixtures/basic.txt +14 -0
  38. data/test/fixtures/custom.txt +15 -0
  39. data/test/fixtures/fwd.txt +0 -0
  40. data/test/fixtures/gb2312_encoding.txt +16 -0
  41. data/test/fixtures/gb2312_encoding_invalid.txt +15 -0
  42. data/test/fixtures/html.txt +16 -0
  43. data/test/fixtures/iso-8859-1.txt +13 -0
  44. data/test/fixtures/mapped.txt +13 -0
  45. data/test/fixtures/multipart.txt +213 -0
  46. data/test/fixtures/multipart2.txt +213 -0
  47. data/test/fixtures/multiple.txt +13 -0
  48. data/test/fixtures/multiple_delivered_to.txt +14 -0
  49. data/test/fixtures/multiple_with_body_recipients.txt +15 -0
  50. data/test/fixtures/reply.txt +16 -0
  51. data/test/fixtures/utf-8.txt +13 -0
  52. data/test/logged_mail_test.rb +63 -0
  53. data/test/mapping_test.rb +129 -0
  54. data/test/message_test.rb +424 -0
  55. data/test/test_helper.rb +54 -0
  56. data/test/transport_test.rb +111 -0
  57. metadata +115 -0
@@ -0,0 +1,157 @@
1
+ module Astrotrain
2
+ class Mapping
3
+ include DataMapper::Resource
4
+
5
+ class << self
6
+ attr_accessor :default_domain
7
+ attr_accessor :transports
8
+ end
9
+
10
+ self.transports = {"HTTP Post" => 'http_post', "Jabber" => 'jabber'}
11
+ self.default_domain = 'astrotrain.com'
12
+
13
+ property :id, Serial
14
+ property :email_user, String, :size => 255, :length => 1..255, :index => :email, :format => /^[\w\.\_\%\+\-]*\*?$/
15
+ property :email_domain, String, :size => 255, :lenght => 1..255, :index => :email, :format => /^[\w\-\_\.]+$/, :default => lambda { default_domain }
16
+ property :destination, String, :size => 255, :length => 1..255
17
+ property :transport, String, :size => 255, :set => transports.values, :default => 'http_post'
18
+ property :separator, String, :size => 255
19
+
20
+ validates_is_unique :email_user, :scope => :email_domain
21
+ validates_format :destination, :as => /^(https?:)\/\/[^\/]+\/?/i, :if => :destination_uses_url?
22
+ validates_format :destination, :as => :email_address, :if => :destination_uses_email?
23
+
24
+ has n, :logged_mails, :order => [:created_at.desc]
25
+
26
+ # returns a mapping for the given array of email addresses
27
+ def self.match(email_addresses)
28
+ email_addresses.each do |email_address|
29
+ email_address.strip!
30
+ email_address.downcase!
31
+ name, domain = email_address.split("@")
32
+ if mapping = match_by_address(name, domain) || match_by_wildcard(name, domain)
33
+ return [mapping, email_address]
34
+ end
35
+ end
36
+ nil
37
+ end
38
+
39
+ # Processes a given message. It finds a mapping, creates a LoggedMail record,
40
+ # and attempts to process the message.
41
+ def self.process(message)
42
+ LoggedMail.from(message) do |logged|
43
+ save_logged = begin
44
+ mapping, recipient = match(message.recipients)
45
+ if mapping
46
+ logged.recipient = recipient
47
+ logged.mapping = mapping
48
+ mapping.process(message, recipient)
49
+ logged.delivered_at = Time.now.utc
50
+ end
51
+ LoggedMail.log_processed # save successfully processed messages?
52
+ rescue
53
+ logged.error_message = "#{$!.class}: #{$!}"
54
+ end
55
+ Astrotrain.callback(:post_processing, message, mapping, logged)
56
+ save_logged
57
+ end
58
+ end
59
+
60
+ # Processes a given message and recipient against the mapping's transport.
61
+ def process(message, recipient)
62
+ Astrotrain.callback(:pre_processing, message, self)
63
+ Transport.process(message, self, recipient)
64
+ end
65
+
66
+ # returns true if the email matches this mapping. Wildcards in the name are allowed.
67
+ # A mapping with foo*@bar.com will match foo@bar.com and food@bar.com, but not foo@baz.com.
68
+ def match?(name, domain)
69
+ email_domain == domain && name =~ email_user_regex
70
+ end
71
+
72
+ def destination_uses_url?
73
+ transport == 'http_post'
74
+ end
75
+
76
+ def destination_uses_email?
77
+ transport == 'jabber'
78
+ end
79
+
80
+ def full_email
81
+ "#{email_user}@#{email_domain}"
82
+ end
83
+
84
+ # Looks for the mapping's separator in the message body and pulls only the content
85
+ # above it. Assuming a separator of '===='...
86
+ #
87
+ # This will be kept
88
+ #
89
+ # On Thu, Sep 3, 2009 at 12:34 AM... (this will be removed)
90
+ # ====
91
+ #
92
+ # > Everything here will be removed.
93
+ #
94
+ def find_reply_from(body)
95
+ return if separator.blank?
96
+ return '' if body.blank?
97
+ lines = body.split("\n")
98
+ delim_line = found_empty = nil
99
+
100
+ (lines.size - 1).downto(0) do |i|
101
+ line = lines[i]
102
+ if !delim_line && line.include?(separator)
103
+ delim_line = i
104
+ elsif delim_line && !found_empty
105
+ delim_line = i
106
+ found_empty = line.strip.blank?
107
+ elsif delim_line && found_empty
108
+ if date_reply_line?(line) || line.strip.blank?
109
+ delim_line = i
110
+ else
111
+ break
112
+ end
113
+ end
114
+ end
115
+
116
+ if delim_line
117
+ body = if delim_line.zero?
118
+ []
119
+ elsif lines.size >= delim_line
120
+ lines[0..delim_line-1]
121
+ else
122
+ lines
123
+ end.join("\n")
124
+ elsif body.frozen?
125
+ body = body.dup
126
+ end
127
+ body.strip!
128
+ body
129
+ end
130
+
131
+ protected
132
+ def self.match_by_address(name, domain)
133
+ first(:email_user => name, :email_domain => domain)
134
+ end
135
+
136
+ def self.match_by_wildcard(name, domain)
137
+ wildcards = all(:email_domain => domain, :email_user.like => "%*")
138
+ wildcards.sort! { |x, y| y.email_user.size <=> x.email_user.size }
139
+ wildcards.detect { |w| w.match?(name, domain) }
140
+ end
141
+
142
+ DATE_LANGUATE_REGEXES = [/^on\b.*wrote\b?:$/i, /^am\b.*schrieb [\w\d\s]+:$/i, /^le\b.*a écrit\b?:$/i]
143
+ def date_reply_line?(line)
144
+ DATE_LANGUATE_REGEXES.any? { |re| line =~ re }
145
+ end
146
+
147
+ def email_user_regex
148
+ @email_user_regex ||= begin
149
+ if email_user['*']
150
+ /^#{email_user.sub /\*/, '(.*)'}$/
151
+ else
152
+ /^#{email_user}$/
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,313 @@
1
+ require 'digest/sha1'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'set'
5
+
6
+ module Astrotrain
7
+ # Wrapper around a TMail object
8
+ class Message
9
+ attr_accessor :body
10
+ attr_reader :mail, :attachments
11
+
12
+ class << self
13
+ attr_reader :queue_path, :archive_path
14
+ attr_accessor :recipient_header_order, :skipped_headers
15
+ end
16
+
17
+ def self.queue_path=(path)
18
+ if path
19
+ path = File.expand_path(path)
20
+ FileUtils.mkdir_p path
21
+ end
22
+ @queue_path = path
23
+ end
24
+
25
+ def self.archive_path=(path)
26
+ if path
27
+ path = File.expand_path(path)
28
+ FileUtils.mkdir_p path
29
+ end
30
+ @archive_path = path
31
+ end
32
+
33
+ self.skipped_headers = Set.new %w(date from subject delivered-to x-original-to received)
34
+ self.recipient_header_order = %w(original_to delivered_to to)
35
+ self.queue_path = File.join(Astrotrain.root, 'queue')
36
+
37
+ # Dumps the raw text into the queue_path. Not really recommended, since you should
38
+ # set the queue_path to the directory your incoming emails are dumped into.
39
+ def self.queue(raw)
40
+ filename = nil
41
+ digest = Digest::SHA1.hexdigest(raw)
42
+ while filename.nil? || File.exist?(filename)
43
+ filename = File.join(queue_path, Digest::SHA1.hexdigest(digest + rand.to_s))
44
+ end
45
+ File.open filename, 'wb' do |f|
46
+ f.write raw
47
+ end
48
+ filename
49
+ end
50
+
51
+ # Parses the given raw email text and processes it with a matching Mapping.
52
+ def self.receive(raw)
53
+ message = parse(raw)
54
+ Astrotrain.callback(:pre_mapping, message)
55
+ Mapping.process(message)
56
+ message
57
+ end
58
+
59
+ # Processes the given file. It parses it by reading the contents, and optionally
60
+ # archives or removes the original file.
61
+ def self.receive_file(path, raw = nil)
62
+ message = receive IO.read(path)
63
+ if archive_path
64
+ daily_archive_path = archive_path / Time.now.year.to_s / Time.now.month.to_s / Time.now.day.to_s
65
+ FileUtils.mkdir_p(daily_archive_path)
66
+ FileUtils.mv path, daily_archive_path / File.basename(path)
67
+ else
68
+ FileUtils.rm_rf path
69
+ end
70
+ message
71
+ end
72
+
73
+ # Parses the raw email headers into a Astrotrain::Message instance.
74
+ def self.parse(raw)
75
+ new Mail.parse(raw)
76
+ end
77
+
78
+ def self.parse_email_addresses(value)
79
+ emails = value.split(",")
80
+ collection = []
81
+ emails.each do |addr|
82
+ addr.strip!
83
+ next if addr.blank?
84
+ header = parse_email_address(addr.to_s)
85
+ collection << unescape(header[:email]) if !header[:email].blank?
86
+ end
87
+ collection
88
+ end
89
+
90
+ def self.parse_email_address(email)
91
+ return {} if email.blank?
92
+ begin
93
+ header = TMail::Address.parse(email)
94
+ {:name => header.name, :email => header.address}
95
+ rescue SyntaxError, TMail::SyntaxError
96
+ email = email.scan(/\<([^\>]+)\>/)[0]
97
+ if email.blank?
98
+ return {:name => nil, :email => nil}
99
+ else
100
+ email = email[0]
101
+ retry
102
+ end
103
+ end
104
+ end
105
+
106
+ # Stolen from Rack/Camping, remove the "+" => " " translation
107
+ def self.unescape(s)
108
+ s.gsub!(/((?:%[0-9a-fA-F]{2})+)/n){
109
+ [$1.delete('%')].pack('H*')
110
+ }
111
+ s
112
+ end
113
+
114
+ def initialize(mail)
115
+ @mail = mail
116
+ @mapping = nil
117
+ @attachments = []
118
+ @recipients = {}
119
+ end
120
+
121
+ # Gets the recipients of an email using the To/Delivered-To/X-Original-To headers.
122
+ # It's not always straightforward which email we want when dealing with filters
123
+ # and forward rules.
124
+ def recipients(order = nil)
125
+ if !@recipients.key?(order)
126
+ order = self.class.recipient_header_order if order.blank?
127
+ recipients = []
128
+
129
+ parse_email_headers recipients_from_body, recipients
130
+ order.each do |key|
131
+ parse_email_headers(send("recipients_from_#{key}"), recipients)
132
+ end
133
+
134
+ recipients.flatten!
135
+ recipients.uniq!
136
+ @recipients[order] = recipients
137
+ else
138
+ @recipients[order]
139
+ end
140
+ end
141
+
142
+ def recipients_from_to
143
+ @recipient_from_to ||= [@mail['to'].to_s]
144
+ end
145
+
146
+ def recipients_from_delivered_to
147
+ @recipient_from_delivered_to ||= begin
148
+ delivered = @mail['Delivered-To']
149
+ if delivered.respond_to?(:first)
150
+ delivered.map! { |a| a.to_s }
151
+ else
152
+ [delivered.to_s]
153
+ end
154
+ end
155
+ end
156
+
157
+ def recipients_from_original_to
158
+ @recipient_from_original_to ||= [@mail['X-Original-To'].to_s]
159
+ end
160
+
161
+ def recipients_from_body
162
+ @recipients_from_body ||= body.scan(/<[\w\.\_\%\+\-]+@[\w\-\_\.]+>/)
163
+ end
164
+
165
+ def sender
166
+ @sender ||= TMail::Unquoter.unquote_and_convert_to(@mail['from'].to_s, "utf-8")
167
+ end
168
+
169
+ def subject
170
+ @mail.subject
171
+ rescue Iconv::InvalidCharacter
172
+ @mail.quoted_subject
173
+ end
174
+
175
+ def body
176
+ @body ||= begin
177
+ process_message_body
178
+ @body
179
+ end
180
+ end
181
+
182
+ def html
183
+ @html ||= begin
184
+ process_message_body
185
+ @html
186
+ end
187
+ end
188
+
189
+ def raw
190
+ @mail.port.to_s
191
+ end
192
+
193
+ def headers
194
+ @headers ||= begin
195
+ h = {}
196
+ @mail.header.each do |key, value|
197
+ next if self.class.skipped_headers.include?(key)
198
+ h[key] = value.to_s
199
+ end
200
+ h
201
+ end
202
+ end
203
+
204
+ class Attachment
205
+ def initialize(part)
206
+ @part = part
207
+ @is_read = false
208
+ end
209
+
210
+ def content_type
211
+ @part.content_type
212
+ end
213
+
214
+ def filename
215
+ @filename ||= @part.type_param("name") || @part.disposition_param('filename')
216
+ end
217
+
218
+ # For IO API compatibility when used with Rest-Client
219
+ def close
220
+ end
221
+
222
+ alias path filename
223
+
224
+ def read(value = nil)
225
+ if read?
226
+ nil
227
+ else
228
+ @is_read = true
229
+ data
230
+ end
231
+ end
232
+
233
+ def read?
234
+ @is_read == true
235
+ end
236
+
237
+ def data
238
+ @part.body
239
+ end
240
+
241
+ def attached?
242
+ !filename.nil?
243
+ end
244
+
245
+ def ==(other)
246
+ super || (filename == other.filename && content_type == other.content_type)
247
+ end
248
+
249
+ def inspect
250
+ %(#<Message::Attachment filename=#{filename.inspect} content_type=#{content_type.inspect}>)
251
+ end
252
+ end
253
+
254
+ protected
255
+ def process_message_body
256
+ if @mail.multipart?
257
+ @attachments.clear
258
+ @body, @html = [], []
259
+ scan_parts(@mail)
260
+ @body = @body.join("\n")
261
+ @html = @html.join("\n")
262
+ else
263
+ @body = @mail.body
264
+ @html = ''
265
+ end
266
+ if !@mail.charset
267
+ @body = convert_to_utf8(@body)
268
+ @html = convert_to_utf8(@html)
269
+ end
270
+ end
271
+
272
+ def scan_parts(message)
273
+ message.parts.each do |part|
274
+ if part.multipart?
275
+ scan_parts(part)
276
+ else
277
+ case part.content_type
278
+ when 'text/plain'
279
+ @body << part.body
280
+ when 'text/html'
281
+ @html << part.body
282
+ else
283
+ att = Attachment.new(part)
284
+ @attachments << att if att.attached?
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ def parse_email_headers(values, collection)
291
+ values.each do |value|
292
+ if !value.blank?
293
+ collection.push *self.class.parse_email_addresses(value)
294
+ end
295
+ end
296
+ end
297
+
298
+ # Attempts to run iconv conversions in common charsets to UTF-8. Needed for
299
+ # those crappy emails that don't properly specify a charset in the headers.
300
+ ICONV_CONVERSIONS = %w(utf-8 ISO-8859-1 ISO-8859-2 ISO-8859-3 ISO-8859-4 ISO-8859-5 ISO-8859-6 ISO-8859-7 ISO-8859-8 ISO-8859-9
301
+ ISO-8859-15 GB2312)
302
+ def convert_to_utf8(s)
303
+ ICONV_CONVERSIONS.each do |from|
304
+ begin
305
+ return Iconv.iconv(ICONV_CONVERSIONS[0], from, s).to_s
306
+ rescue Iconv::IllegalSequence
307
+ ensure
308
+ s
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,48 @@
1
+ module Astrotrain
2
+ # custom subclass of TMail::Mail that fixes some bugs. The fixes were pushed upstream,
3
+ # and this class will go away once the gem is released.
4
+ class Mail < TMail::Mail
5
+ def charset( default = nil )
6
+ if h = @header['content-type']
7
+ h['charset'] || mime_version_charset || default
8
+ else
9
+ mime_version_charset || default
10
+ end
11
+ end
12
+
13
+ # some weird emails come with the charset specified in the mime-version header:
14
+ #
15
+ # #<TMail::MimeVersionHeader "1.0\n charset=\"gb2312\"">
16
+ #
17
+ def mime_version_charset
18
+ if header['mime-version'].inspect =~ /charset=('|\\")?([^\\"']+)/
19
+ $2
20
+ end
21
+ end
22
+
23
+ # copied from TMail::Mail, uses #charset instead of #sub_header
24
+ def unquoted_body(to_charset = 'utf-8')
25
+ from_charset = charset
26
+ case (content_transfer_encoding || "7bit").downcase
27
+ when "quoted-printable"
28
+ # the default charset is set to iso-8859-1 instead of 'us-ascii'.
29
+ # This is needed as many mailer do not set the charset but send in ISO. This is only used if no charset is set.
30
+ if !from_charset.blank? && from_charset.downcase == 'us-ascii'
31
+ from_charset = 'iso-8859-1'
32
+ end
33
+
34
+ TMail::Unquoter.unquote_quoted_printable_and_convert_to(quoted_body,
35
+ to_charset, from_charset, true)
36
+ when "base64"
37
+ TMail::Unquoter.unquote_base64_and_convert_to(quoted_body, to_charset,
38
+ from_charset)
39
+ when "7bit", "8bit"
40
+ TMail::Unquoter.convert_to(quoted_body, to_charset, from_charset)
41
+ when "binary"
42
+ quoted_body
43
+ else
44
+ quoted_body
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/astrotrain.rb ADDED
@@ -0,0 +1,55 @@
1
+ module Astrotrain
2
+ CALLBACK_TYPES = [:pre_mapping, :pre_processing, :post_processing]
3
+ class << self
4
+ attr_accessor :root, :lib_root, :callbacks
5
+ end
6
+
7
+ def self.load(root = Dir.pwd)
8
+ self.root = File.expand_path(root)
9
+ self.lib_root = File.expand_path(File.dirname(__FILE__))
10
+ load_dependencies
11
+ yield if block_given?
12
+ %w(tmail message mapping logged_mail mapping/transport mapping/http_post mapping/jabber).each do |lib|
13
+ require "astrotrain/#{lib}"
14
+ end
15
+ Astrotrain::Mail::ALLOW_MULTIPLE['delivered-to'] = true
16
+ end
17
+
18
+ def self.callback(name, *args, &block)
19
+ found = callbacks[name]
20
+ if block
21
+ found << block
22
+ else
23
+ found.each { |cback| cback.call(*args) }
24
+ end
25
+ found
26
+ end
27
+
28
+ def self.clear_callbacks
29
+ self.callbacks = CALLBACK_TYPES.inject({}) { |memo, ctype| memo.update(ctype => []) }
30
+ end
31
+
32
+ clear_callbacks
33
+
34
+ private
35
+ # help me ryan tomayko, you're my only help
36
+ def self.load_dependencies
37
+ require 'rubygems'
38
+ gem 'addressable', '2.0.2'
39
+ gem "tmail", "1.2.3.1"
40
+ gem "xmpp4r-simple", "0.8.8"
41
+
42
+ dm_ver = "0.9.11"
43
+ gem "dm-core", dm_ver # The datamapper ORM
44
+ gem "dm-aggregates", dm_ver # Provides your DM models with count, sum, avg, min, max, etc.
45
+ gem "dm-timestamps", dm_ver # Automatically populate created_at, created_on, etc. when those properties are present.
46
+ gem "dm-types", dm_ver # Provides additional types, including csv, json, yaml.
47
+ gem "dm-validations", dm_ver # Validation framework
48
+
49
+ $LOAD_PATH.unshift File.join(lib_root, 'vendor', 'rest-client', 'lib')
50
+
51
+ %w(dm-core dm-aggregates dm-timestamps dm-types dm-validations xmpp4r-simple tmail rest_client).each do |lib|
52
+ require lib
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,104 @@
1
+ = REST Client -- simple DSL for accessing REST resources
2
+
3
+ A simple REST client for Ruby, inspired by the Sinatra's microframework style
4
+ of specifying actions: get, put, post, delete.
5
+
6
+ == Usage: Raw URL
7
+
8
+ require 'rest_client'
9
+
10
+ RestClient.get 'http://example.com/resource'
11
+ RestClient.get 'https://user:password@example.com/private/resource'
12
+
13
+ RestClient.post 'http://example.com/resource', :param1 => 'one', :nested => { :param2 => 'two' }
14
+
15
+ RestClient.delete 'http://example.com/resource'
16
+
17
+ == Multipart
18
+
19
+ Yeah, that's right! This does multipart sends for you!
20
+
21
+ RestClient.post '/data', :myfile => File.new("/path/to/image.jpg")
22
+
23
+ This does two things for you:
24
+
25
+ * Auto-detects that you have a File value sends it as multipart
26
+ * Auto-detects the mime of the file and sets it in the HEAD of the payload for each entry
27
+
28
+ If you are sending params that do not contain a File object but the payload needs to be multipart then:
29
+
30
+ RestClient.post '/data', :foo => 'bar', :multipart => true
31
+
32
+ == Streaming downloads
33
+
34
+ RestClient.get('http://some/resource/lotsofdata') do |res|
35
+ res.read_body do |chunk|
36
+ .. do something with chunk ..
37
+ end
38
+ end
39
+
40
+ See RestClient module docs for more details.
41
+
42
+ == Usage: ActiveResource-Style
43
+
44
+ resource = RestClient::Resource.new 'http://example.com/resource'
45
+ resource.get
46
+
47
+ private_resource = RestClient::Resource.new 'https://example.com/private/resource', 'user', 'pass'
48
+ private_resource.put File.read('pic.jpg'), :content_type => 'image/jpg'
49
+
50
+ See RestClient::Resource module docs for details.
51
+
52
+ == Usage: Resource Nesting
53
+
54
+ site = RestClient::Resource.new('http://example.com')
55
+ site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
56
+
57
+ See RestClient::Resource docs for details.
58
+
59
+ == Shell
60
+
61
+ The restclient shell command gives an IRB session with RestClient already loaded:
62
+
63
+ $ restclient
64
+ >> RestClient.get 'http://example.com'
65
+
66
+ Specify a URL argument for get/post/put/delete on that resource:
67
+
68
+ $ restclient http://example.com
69
+ >> put '/resource', 'data'
70
+
71
+ Add a user and password for authenticated resources:
72
+
73
+ $ restclient https://example.com user pass
74
+ >> delete '/private/resource'
75
+
76
+ Create ~/.restclient for named sessions:
77
+
78
+ sinatra:
79
+ url: http://localhost:4567
80
+ rack:
81
+ url: http://localhost:9292
82
+ private_site:
83
+ url: http://example.com
84
+ username: user
85
+ password: pass
86
+
87
+ Then invoke:
88
+
89
+ $ restclient private_site
90
+
91
+ == Meta
92
+
93
+ Written by Adam Wiggins (adam at heroku dot com)
94
+
95
+ Major modifications by Blake Mizerany
96
+
97
+ Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, and Aman Gupta
98
+
99
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
100
+
101
+ http://rest-client.heroku.com
102
+
103
+ http://github.com/adamwiggins/rest-client
104
+