astrotrain 0.3.1
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.
- data/.gitignore +26 -0
- data/LICENSE +20 -0
- data/README +47 -0
- data/Rakefile +122 -0
- data/VERSION +1 -0
- data/astrotrain.gemspec +129 -0
- data/config/sample.rb +12 -0
- data/lib/astrotrain.rb +53 -0
- data/lib/astrotrain/api.rb +52 -0
- data/lib/astrotrain/logged_mail.rb +46 -0
- data/lib/astrotrain/mapping.rb +157 -0
- data/lib/astrotrain/mapping/http_post.rb +18 -0
- data/lib/astrotrain/mapping/jabber.rb +28 -0
- data/lib/astrotrain/mapping/transport.rb +55 -0
- data/lib/astrotrain/message.rb +330 -0
- data/lib/astrotrain/tmail.rb +58 -0
- data/lib/astrotrain/worker.rb +65 -0
- data/lib/vendor/rest-client/README.rdoc +104 -0
- data/lib/vendor/rest-client/Rakefile +84 -0
- data/lib/vendor/rest-client/bin/restclient +65 -0
- data/lib/vendor/rest-client/foo.diff +66 -0
- data/lib/vendor/rest-client/lib/rest_client.rb +188 -0
- data/lib/vendor/rest-client/lib/rest_client/net_http_ext.rb +23 -0
- data/lib/vendor/rest-client/lib/rest_client/payload.rb +185 -0
- data/lib/vendor/rest-client/lib/rest_client/request_errors.rb +75 -0
- data/lib/vendor/rest-client/lib/rest_client/resource.rb +103 -0
- data/lib/vendor/rest-client/rest-client.gemspec +18 -0
- data/lib/vendor/rest-client/spec/base.rb +5 -0
- data/lib/vendor/rest-client/spec/master_shake.jpg +0 -0
- data/lib/vendor/rest-client/spec/payload_spec.rb +71 -0
- data/lib/vendor/rest-client/spec/request_errors_spec.rb +44 -0
- data/lib/vendor/rest-client/spec/resource_spec.rb +52 -0
- data/lib/vendor/rest-client/spec/rest_client_spec.rb +219 -0
- data/test/api_test.rb +28 -0
- data/test/fixtures/apple_multipart.txt +100 -0
- data/test/fixtures/bad_content_type.txt +27 -0
- data/test/fixtures/basic.txt +14 -0
- data/test/fixtures/custom.txt +15 -0
- data/test/fixtures/fwd.txt +0 -0
- data/test/fixtures/gb2312_encoding.txt +16 -0
- data/test/fixtures/gb2312_encoding_invalid.txt +15 -0
- data/test/fixtures/html.txt +16 -0
- data/test/fixtures/iso-8859-1.txt +13 -0
- data/test/fixtures/mapped.txt +13 -0
- data/test/fixtures/multipart.txt +213 -0
- data/test/fixtures/multipart2.txt +213 -0
- data/test/fixtures/multiple.txt +13 -0
- data/test/fixtures/multiple_delivered_to.txt +14 -0
- data/test/fixtures/multiple_with_body_recipients.txt +15 -0
- data/test/fixtures/reply.txt +16 -0
- data/test/fixtures/utf-8.txt +13 -0
- data/test/logged_mail_test.rb +67 -0
- data/test/mapping_test.rb +129 -0
- data/test/message_test.rb +440 -0
- data/test/test_helper.rb +57 -0
- data/test/transport_test.rb +111 -0
- metadata +225 -0
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
|
4
|
+
require File.dirname(__FILE__) + '/rest_client/resource'
|
5
|
+
require File.dirname(__FILE__) + '/rest_client/request_errors'
|
6
|
+
require File.dirname(__FILE__) + '/rest_client/payload'
|
7
|
+
require File.dirname(__FILE__) + '/rest_client/net_http_ext'
|
8
|
+
|
9
|
+
# This module's static methods are the entry point for using the REST client.
|
10
|
+
#
|
11
|
+
# # GET
|
12
|
+
# xml = RestClient.get 'http://example.com/resource'
|
13
|
+
# jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
|
14
|
+
#
|
15
|
+
# # authentication and SSL
|
16
|
+
# RestClient.get 'https://user:password@example.com/private/resource'
|
17
|
+
#
|
18
|
+
# # POST or PUT with a hash sends parameters as a urlencoded form body
|
19
|
+
# RestClient.post 'http://example.com/resource', :param1 => 'one'
|
20
|
+
#
|
21
|
+
# # nest hash parameters
|
22
|
+
# RestClient.post 'http://example.com/resource', :nested => { :param1 => 'one' }
|
23
|
+
#
|
24
|
+
# # POST and PUT with raw payloads
|
25
|
+
# RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain'
|
26
|
+
# RestClient.post 'http://example.com/resource.xml', xml_doc
|
27
|
+
# RestClient.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf'
|
28
|
+
#
|
29
|
+
# # DELETE
|
30
|
+
# RestClient.delete 'http://example.com/resource'
|
31
|
+
#
|
32
|
+
# For live tests of RestClient, try using http://rest-test.heroku.com, which echoes back information about the rest call:
|
33
|
+
#
|
34
|
+
# >> RestClient.put 'http://rest-test.heroku.com/resource', :foo => 'baz'
|
35
|
+
# => "PUT http://rest-test.heroku.com/resource with a 7 byte payload, content type application/x-www-form-urlencoded {\"foo\"=>\"baz\"}"
|
36
|
+
#
|
37
|
+
module RestClient
|
38
|
+
def self.get(url, headers={}, &b)
|
39
|
+
Request.execute(:method => :get,
|
40
|
+
:url => url,
|
41
|
+
:headers => headers, &b)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.post(url, payload, headers={}, &b)
|
45
|
+
Request.execute(:method => :post,
|
46
|
+
:url => url,
|
47
|
+
:payload => payload,
|
48
|
+
:headers => headers, &b)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.put(url, payload, headers={}, &b)
|
52
|
+
Request.execute(:method => :put,
|
53
|
+
:url => url,
|
54
|
+
:payload => payload,
|
55
|
+
:headers => headers, &b)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.delete(url, headers={}, &b)
|
59
|
+
Request.execute(:method => :delete,
|
60
|
+
:url => url,
|
61
|
+
:headers => headers, &b)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Internal class used to build and execute the request.
|
65
|
+
class Request
|
66
|
+
attr_reader :method, :url, :headers, :user, :password
|
67
|
+
|
68
|
+
def self.execute(args, &b)
|
69
|
+
new(args).execute(&b)
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize(args)
|
73
|
+
@method = args[:method] or raise ArgumentError, "must pass :method"
|
74
|
+
@url = args[:url] or raise ArgumentError, "must pass :url"
|
75
|
+
@payload = Payload.generate(args[:payload] || '')
|
76
|
+
@headers = args[:headers] || {}
|
77
|
+
@user = args[:user]
|
78
|
+
@password = args[:password]
|
79
|
+
end
|
80
|
+
|
81
|
+
def execute(&b)
|
82
|
+
execute_inner(&b)
|
83
|
+
rescue Redirect => e
|
84
|
+
@url = e.url
|
85
|
+
execute(&b)
|
86
|
+
end
|
87
|
+
|
88
|
+
def execute_inner(&b)
|
89
|
+
uri = parse_url_with_auth(url)
|
90
|
+
transmit(uri, net_http_class(method).new(uri.request_uri, make_headers(headers)), payload, &b)
|
91
|
+
end
|
92
|
+
|
93
|
+
def make_headers(user_headers)
|
94
|
+
final = {}
|
95
|
+
merged = default_headers.merge(user_headers)
|
96
|
+
merged.keys.each do |key|
|
97
|
+
final[key.to_s.gsub(/_/, '-').capitalize] = merged[key]
|
98
|
+
end
|
99
|
+
final
|
100
|
+
end
|
101
|
+
|
102
|
+
def net_http_class(method)
|
103
|
+
Net::HTTP.const_get(method.to_s.capitalize)
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse_url(url)
|
107
|
+
url = "http://#{url}" unless url.match(/^http/)
|
108
|
+
URI.parse(url)
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse_url_with_auth(url)
|
112
|
+
uri = parse_url(url)
|
113
|
+
@user = uri.user if uri.user
|
114
|
+
@password = uri.password if uri.password
|
115
|
+
uri
|
116
|
+
end
|
117
|
+
|
118
|
+
def process_payload(p=nil, parent_key=nil)
|
119
|
+
unless p.is_a?(Hash)
|
120
|
+
p
|
121
|
+
else
|
122
|
+
@headers[:content_type] ||= 'application/x-www-form-urlencoded'
|
123
|
+
p.keys.map do |k|
|
124
|
+
key = parent_key ? "#{parent_key}[#{k}]" : k
|
125
|
+
if p[k].is_a? Hash
|
126
|
+
process_payload(p[k], key)
|
127
|
+
else
|
128
|
+
value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
129
|
+
"#{key}=#{value}"
|
130
|
+
end
|
131
|
+
end.join("&")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def transmit(uri, req, payload, &b)
|
136
|
+
setup_credentials(req)
|
137
|
+
|
138
|
+
net = Net::HTTP.new(uri.host, uri.port)
|
139
|
+
net.use_ssl = uri.is_a?(URI::HTTPS)
|
140
|
+
net.start do |http|
|
141
|
+
## Ok. I know this is weird but it's a hack for now
|
142
|
+
## this lets process_result determine if it should read the body
|
143
|
+
## into memory or not
|
144
|
+
process_result(http.request(req, payload || "", &b), &b)
|
145
|
+
end
|
146
|
+
rescue EOFError
|
147
|
+
raise RestClient::ServerBrokeConnection
|
148
|
+
rescue Timeout::Error
|
149
|
+
raise RestClient::RequestTimeout
|
150
|
+
ensure
|
151
|
+
payload.close
|
152
|
+
end
|
153
|
+
|
154
|
+
def setup_credentials(req)
|
155
|
+
req.basic_auth(user, password) if user
|
156
|
+
end
|
157
|
+
|
158
|
+
def process_result(res, &b)
|
159
|
+
if %w(200 201 202).include? res.code
|
160
|
+
return res.body unless b
|
161
|
+
elsif %w(301 302 303).include? res.code
|
162
|
+
url = res.header['Location']
|
163
|
+
|
164
|
+
if url !~ /^http/
|
165
|
+
uri = URI.parse(@url)
|
166
|
+
uri.path = "/#{url}".squeeze('/')
|
167
|
+
url = uri.to_s
|
168
|
+
end
|
169
|
+
|
170
|
+
raise Redirect, url
|
171
|
+
elsif res.code == "401"
|
172
|
+
raise Unauthorized
|
173
|
+
elsif res.code == "404"
|
174
|
+
raise ResourceNotFound
|
175
|
+
else
|
176
|
+
raise RequestFailed, res
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def payload
|
181
|
+
@payload
|
182
|
+
end
|
183
|
+
|
184
|
+
def default_headers
|
185
|
+
@payload.headers.merge({ :accept => 'application/xml' })
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#
|
2
|
+
# Replace the request method in Net::HTTP to sniff the body type
|
3
|
+
# and set the stream if appropriate
|
4
|
+
#
|
5
|
+
# Taken from:
|
6
|
+
# http://www.missiondata.com/blog/ruby/29/streaming-data-to-s3-with-ruby/
|
7
|
+
|
8
|
+
Net::HTTP.ssl_context_accessor(:tmp_dh_callback)
|
9
|
+
|
10
|
+
module Net
|
11
|
+
class HTTP
|
12
|
+
alias __request__ request
|
13
|
+
|
14
|
+
def request(req, body=nil, &block)
|
15
|
+
if body != nil && body.respond_to?(:read)
|
16
|
+
req.body_stream = body
|
17
|
+
return __request__(req, nil, &block)
|
18
|
+
else
|
19
|
+
return __request__(req, body, &block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
require "stringio"
|
3
|
+
|
4
|
+
module RestClient
|
5
|
+
module Payload
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def generate(params)
|
9
|
+
if params.is_a?(String)
|
10
|
+
Base.new(params)
|
11
|
+
elsif params.delete(:multipart) == true ||
|
12
|
+
params.any? { |_,v| v.respond_to?(:path) && v.respond_to?(:read) }
|
13
|
+
Multipart.new(params)
|
14
|
+
else
|
15
|
+
UrlEncoded.new(params)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Base
|
20
|
+
def initialize(params)
|
21
|
+
build_stream(params)
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_stream(params)
|
25
|
+
@stream = StringIO.new(params)
|
26
|
+
@stream.seek(0)
|
27
|
+
end
|
28
|
+
|
29
|
+
def read(bytes=nil)
|
30
|
+
@stream.read(bytes)
|
31
|
+
end
|
32
|
+
alias :to_s :read
|
33
|
+
|
34
|
+
def escape(v)
|
35
|
+
URI.escape(v.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
36
|
+
end
|
37
|
+
|
38
|
+
def headers
|
39
|
+
{ 'Content-Length' => size.to_s }
|
40
|
+
end
|
41
|
+
|
42
|
+
def size
|
43
|
+
@stream.size
|
44
|
+
end
|
45
|
+
alias :length :size
|
46
|
+
|
47
|
+
def close
|
48
|
+
@stream.close
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class UrlEncoded < Base
|
53
|
+
def build_stream(params)
|
54
|
+
@stream = StringIO.new(params.map do |k,v|
|
55
|
+
"#{escape(k)}=#{escape(v)}"
|
56
|
+
end.join("&"))
|
57
|
+
@stream.seek(0)
|
58
|
+
end
|
59
|
+
|
60
|
+
def headers
|
61
|
+
super.merge({ 'Content-Type' => 'application/x-www-form-urlencoded' })
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class Multipart < Base
|
66
|
+
EOL = "\r\n"
|
67
|
+
|
68
|
+
def build_stream(params)
|
69
|
+
b = "--#{boundary}"
|
70
|
+
|
71
|
+
@stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
|
72
|
+
@stream.write(b)
|
73
|
+
params.each do |k,v|
|
74
|
+
@stream.write(EOL)
|
75
|
+
if v.respond_to?(:read) && v.respond_to?(:path)
|
76
|
+
create_file_field(@stream, k,v)
|
77
|
+
else
|
78
|
+
create_regular_field(@stream, k,v)
|
79
|
+
end
|
80
|
+
@stream.write(EOL + b)
|
81
|
+
end
|
82
|
+
@stream.write('--')
|
83
|
+
@stream.write(EOL)
|
84
|
+
@stream.seek(0)
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_regular_field(s, k, v)
|
88
|
+
s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"")
|
89
|
+
s.write(EOL)
|
90
|
+
s.write(EOL)
|
91
|
+
s.write(v)
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_file_field(s, k, v)
|
95
|
+
begin
|
96
|
+
s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}")
|
97
|
+
s.write("Content-Type: #{mime_for(v.path)}#{EOL}")
|
98
|
+
s.write(EOL)
|
99
|
+
while data = v.read(8124)
|
100
|
+
s.write(data)
|
101
|
+
end
|
102
|
+
ensure
|
103
|
+
v.close
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def mime_for(path)
|
108
|
+
ext = File.extname(path)[1..-1]
|
109
|
+
MIME_TYPES[ext] || 'text/plain'
|
110
|
+
end
|
111
|
+
|
112
|
+
def boundary
|
113
|
+
@boundary ||= rand(1_000_000).to_s
|
114
|
+
end
|
115
|
+
|
116
|
+
def headers
|
117
|
+
super.merge({'Content-Type' => %Q{multipart/form-data; boundary="#{boundary}"}})
|
118
|
+
end
|
119
|
+
|
120
|
+
def close
|
121
|
+
@stream.close
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# :stopdoc:
|
126
|
+
# From WEBrick.
|
127
|
+
MIME_TYPES = {
|
128
|
+
"ai" => "application/postscript",
|
129
|
+
"asc" => "text/plain",
|
130
|
+
"avi" => "video/x-msvideo",
|
131
|
+
"bin" => "application/octet-stream",
|
132
|
+
"bmp" => "image/bmp",
|
133
|
+
"class" => "application/octet-stream",
|
134
|
+
"cer" => "application/pkix-cert",
|
135
|
+
"crl" => "application/pkix-crl",
|
136
|
+
"crt" => "application/x-x509-ca-cert",
|
137
|
+
"css" => "text/css",
|
138
|
+
"dms" => "application/octet-stream",
|
139
|
+
"doc" => "application/msword",
|
140
|
+
"dvi" => "application/x-dvi",
|
141
|
+
"eps" => "application/postscript",
|
142
|
+
"etx" => "text/x-setext",
|
143
|
+
"exe" => "application/octet-stream",
|
144
|
+
"gif" => "image/gif",
|
145
|
+
"gz" => "application/x-gzip",
|
146
|
+
"htm" => "text/html",
|
147
|
+
"html" => "text/html",
|
148
|
+
"jpe" => "image/jpeg",
|
149
|
+
"jpeg" => "image/jpeg",
|
150
|
+
"jpg" => "image/jpeg",
|
151
|
+
"js" => "text/javascript",
|
152
|
+
"lha" => "application/octet-stream",
|
153
|
+
"lzh" => "application/octet-stream",
|
154
|
+
"mov" => "video/quicktime",
|
155
|
+
"mpe" => "video/mpeg",
|
156
|
+
"mpeg" => "video/mpeg",
|
157
|
+
"mpg" => "video/mpeg",
|
158
|
+
"pbm" => "image/x-portable-bitmap",
|
159
|
+
"pdf" => "application/pdf",
|
160
|
+
"pgm" => "image/x-portable-graymap",
|
161
|
+
"png" => "image/png",
|
162
|
+
"pnm" => "image/x-portable-anymap",
|
163
|
+
"ppm" => "image/x-portable-pixmap",
|
164
|
+
"ppt" => "application/vnd.ms-powerpoint",
|
165
|
+
"ps" => "application/postscript",
|
166
|
+
"qt" => "video/quicktime",
|
167
|
+
"ras" => "image/x-cmu-raster",
|
168
|
+
"rb" => "text/plain",
|
169
|
+
"rd" => "text/plain",
|
170
|
+
"rtf" => "application/rtf",
|
171
|
+
"sgm" => "text/sgml",
|
172
|
+
"sgml" => "text/sgml",
|
173
|
+
"tif" => "image/tiff",
|
174
|
+
"tiff" => "image/tiff",
|
175
|
+
"txt" => "text/plain",
|
176
|
+
"xbm" => "image/x-xbitmap",
|
177
|
+
"xls" => "application/vnd.ms-excel",
|
178
|
+
"xml" => "text/xml",
|
179
|
+
"xpm" => "image/x-xpixmap",
|
180
|
+
"xwd" => "image/x-xwindowdump",
|
181
|
+
"zip" => "application/zip",
|
182
|
+
}
|
183
|
+
# :startdoc:
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module RestClient
|
2
|
+
# This is the base RestClient exception class. Rescue it if you want to
|
3
|
+
# catch any exception that your request might raise
|
4
|
+
class Exception < RuntimeError
|
5
|
+
def message(default=nil)
|
6
|
+
self.class::ErrorMessage
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# A redirect was encountered; caught by execute to retry with the new url.
|
11
|
+
class Redirect < Exception
|
12
|
+
ErrorMessage = "Redirect"
|
13
|
+
|
14
|
+
attr_accessor :url
|
15
|
+
def initialize(url)
|
16
|
+
@url = url
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Authorization is required to access the resource specified.
|
21
|
+
class Unauthorized < Exception
|
22
|
+
ErrorMessage = 'Unauthorized'
|
23
|
+
end
|
24
|
+
|
25
|
+
# No resource was found at the given URL.
|
26
|
+
class ResourceNotFound < Exception
|
27
|
+
ErrorMessage = 'Resource not found'
|
28
|
+
end
|
29
|
+
|
30
|
+
# The server broke the connection prior to the request completing.
|
31
|
+
class ServerBrokeConnection < Exception
|
32
|
+
ErrorMessage = 'Server broke connection'
|
33
|
+
end
|
34
|
+
|
35
|
+
# The server took too long to respond.
|
36
|
+
class RequestTimeout < Exception
|
37
|
+
ErrorMessage = 'Request timed out'
|
38
|
+
end
|
39
|
+
|
40
|
+
# The request failed, meaning the remote HTTP server returned a code other
|
41
|
+
# than success, unauthorized, or redirect.
|
42
|
+
#
|
43
|
+
# The exception message attempts to extract the error from the XML, using
|
44
|
+
# format returned by Rails: <errors><error>some message</error></errors>
|
45
|
+
#
|
46
|
+
# You can get the status code by e.http_code, or see anything about the
|
47
|
+
# response via e.response. For example, the entire result body (which is
|
48
|
+
# probably an HTML error page) is e.response.body.
|
49
|
+
class RequestFailed < Exception
|
50
|
+
attr_accessor :response
|
51
|
+
|
52
|
+
def initialize(response=nil)
|
53
|
+
@response = response
|
54
|
+
end
|
55
|
+
|
56
|
+
def http_code
|
57
|
+
@response.code.to_i if @response
|
58
|
+
end
|
59
|
+
|
60
|
+
def message
|
61
|
+
"HTTP status code #{http_code}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
message
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# backwards compatibility
|
71
|
+
class RestClient::Request
|
72
|
+
Redirect = RestClient::Redirect
|
73
|
+
Unauthorized = RestClient::Unauthorized
|
74
|
+
RequestFailed = RestClient::RequestFailed
|
75
|
+
end
|