postmark 0.9.19 → 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.
Files changed (41) hide show
  1. data/.travis.yml +8 -0
  2. data/CHANGELOG.rdoc +20 -0
  3. data/Gemfile +6 -0
  4. data/README.md +351 -91
  5. data/VERSION +1 -1
  6. data/lib/postmark.rb +40 -132
  7. data/lib/postmark/api_client.rb +162 -0
  8. data/lib/postmark/bounce.rb +20 -17
  9. data/lib/postmark/handlers/mail.rb +10 -3
  10. data/lib/postmark/helpers/hash_helper.rb +35 -0
  11. data/lib/postmark/helpers/message_helper.rb +62 -0
  12. data/lib/postmark/http_client.rb +44 -28
  13. data/lib/postmark/inbound.rb +21 -0
  14. data/lib/postmark/inflector.rb +28 -0
  15. data/lib/postmark/message_extensions/mail.rb +50 -5
  16. data/lib/postmark/message_extensions/shared.rb +23 -28
  17. data/lib/postmark/version.rb +1 -1
  18. data/postmark.gemspec +4 -7
  19. data/spec/data/empty.gif +0 -0
  20. data/spec/integration/api_client_hashes_spec.rb +101 -0
  21. data/spec/integration/api_client_messages_spec.rb +127 -0
  22. data/spec/integration/mail_delivery_method_spec.rb +80 -0
  23. data/spec/spec_helper.rb +15 -5
  24. data/spec/support/helpers.rb +11 -0
  25. data/spec/{shared_examples.rb → support/shared_examples.rb} +0 -0
  26. data/spec/unit/postmark/api_client_spec.rb +246 -0
  27. data/spec/unit/postmark/bounce_spec.rb +142 -0
  28. data/spec/unit/postmark/handlers/mail_spec.rb +39 -0
  29. data/spec/unit/postmark/helpers/hash_helper_spec.rb +34 -0
  30. data/spec/unit/postmark/helpers/message_helper_spec.rb +115 -0
  31. data/spec/unit/postmark/http_client_spec.rb +204 -0
  32. data/spec/unit/postmark/inbound_spec.rb +88 -0
  33. data/spec/unit/postmark/inflector_spec.rb +35 -0
  34. data/spec/unit/postmark/json_spec.rb +37 -0
  35. data/spec/unit/postmark/message_extensions/mail_spec.rb +205 -0
  36. data/spec/unit/postmark_spec.rb +164 -0
  37. metadata +45 -93
  38. data/lib/postmark/attachments_fix_for_mail.rb +0 -48
  39. data/lib/postmark/message_extensions/tmail.rb +0 -115
  40. data/spec/bounce_spec.rb +0 -53
  41. data/spec/postmark_spec.rb +0 -253
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe Postmark::Bounce do
4
+ let(:bounce_data) { {:type => "HardBounce",
5
+ :message_id => "d12c2f1c-60f3-4258-b163-d17052546ae4",
6
+ :type_code => 1,
7
+ :details => "test bounce",
8
+ :email => "jim@test.com",
9
+ :bounced_at => "2013-04-22 18:06:48 +0800",
10
+ :dump_available => true,
11
+ :inactive => false,
12
+ :can_activate => true,
13
+ :id => 42,
14
+ :subject => "Hello from our app!"} }
15
+ let(:bounce_data_postmark) { Postmark::HashHelper.to_postmark(bounce_data) }
16
+ let(:bounces_data) { [bounce_data, bounce_data, bounce_data] }
17
+
18
+ let(:bounce) { Postmark::Bounce.new(bounce_data) }
19
+
20
+ subject { bounce }
21
+
22
+ context "attr readers" do
23
+
24
+ it { should respond_to(:email) }
25
+ it { should respond_to(:bounced_at) }
26
+ it { should respond_to(:type) }
27
+ it { should respond_to(:details) }
28
+ it { should respond_to(:name) }
29
+ it { should respond_to(:id) }
30
+ it { should respond_to(:server_id) }
31
+ it { should respond_to(:tag) }
32
+ it { should respond_to(:message_id) }
33
+ it { should respond_to(:subject) }
34
+
35
+ end
36
+
37
+ context "given a bounce created from bounce_data" do
38
+
39
+ it 'is not inactive' do
40
+ should_not be_inactive
41
+ end
42
+
43
+ it 'allows to activate the bounce' do
44
+ subject.can_activate?.should be_true
45
+ end
46
+
47
+ it 'has an available dump' do
48
+ subject.dump_available?.should be_true
49
+ end
50
+
51
+ its(:type) { should eq bounce_data[:type] }
52
+ its(:message_id) { should eq bounce_data[:message_id] }
53
+ its(:details) { should eq bounce_data[:details] }
54
+ its(:email) { should eq bounce_data[:email] }
55
+ its(:bounced_at) { should == Time.parse(bounce_data[:bounced_at]) }
56
+ its(:id) { should eq bounce_data[:id] }
57
+ its(:subject) { should eq bounce_data[:subject] }
58
+
59
+ end
60
+
61
+ context "given a bounce created from bounce_data_postmark" do
62
+ subject { Postmark::Bounce.new(bounce_data_postmark) }
63
+
64
+ it 'is not inactive' do
65
+ should_not be_inactive
66
+ end
67
+
68
+ it 'allows to activate the bounce' do
69
+ subject.can_activate?.should be_true
70
+ end
71
+
72
+ it 'has an available dump' do
73
+ subject.dump_available?.should be_true
74
+ end
75
+
76
+ its(:type) { should eq bounce_data[:type] }
77
+ its(:message_id) { should eq bounce_data[:message_id] }
78
+ its(:details) { should eq bounce_data[:details] }
79
+ its(:email) { should eq bounce_data[:email] }
80
+ its(:bounced_at) { should == Time.parse(bounce_data[:bounced_at]) }
81
+ its(:id) { should eq bounce_data[:id] }
82
+ its(:subject) { should eq bounce_data[:subject] }
83
+
84
+ end
85
+
86
+ describe "#dump" do
87
+
88
+ let(:bounce_body) { mock }
89
+ let(:response) { {:body => bounce_body} }
90
+ let(:api_client) { Postmark.api_client }
91
+
92
+ it "calls #dump_bounce on shared api_client instance" do
93
+ Postmark.api_client.should_receive(:dump_bounce).with(bounce.id) { response }
94
+ bounce.dump.should == bounce_body
95
+ end
96
+
97
+ end
98
+
99
+ describe "#activate" do
100
+
101
+ let(:api_client) { Postmark.api_client }
102
+
103
+ it "calls #activate_bounce on shared api_client instance" do
104
+ api_client.should_receive(:activate_bounce).with(bounce.id) { bounce_data }
105
+ bounce.activate.should be_a Postmark::Bounce
106
+ end
107
+
108
+ end
109
+
110
+ describe ".find" do
111
+ let(:api_client) { Postmark.api_client }
112
+
113
+ it "calls #get_bounce on shared api_client instance" do
114
+ api_client.should_receive(:get_bounce).with(42) { bounce_data }
115
+ Postmark::Bounce.find(42).should be_a Postmark::Bounce
116
+ end
117
+ end
118
+
119
+ describe ".all" do
120
+
121
+ let(:response) { bounces_data }
122
+ let(:api_client) { Postmark.api_client }
123
+
124
+ it "calls #get_bounces on shared api_client instance" do
125
+ api_client.should_receive(:get_bounces) { response }
126
+ Postmark::Bounce.all.should have(3).bounces
127
+ end
128
+
129
+ end
130
+
131
+ describe ".tags" do
132
+
133
+ let(:api_client) { Postmark.api_client }
134
+ let(:tags) { ["tag1", "tag2"] }
135
+
136
+ it "calls #get_bounced_tags on shared api_client instance" do
137
+ api_client.should_receive(:get_bounced_tags) { tags }
138
+ Postmark::Bounce.tags.should == tags
139
+ end
140
+ end
141
+
142
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mail::Postmark do
4
+ let(:message) do
5
+ Mail.new do
6
+ from "sheldon@bigbangtheory.com"
7
+ to "lenard@bigbangtheory.com"
8
+ subject "Hello!"
9
+ body "Hello Sheldon!"
10
+ end
11
+ end
12
+
13
+ it "can be set as delivery_method" do
14
+ message.delivery_method Mail::Postmark
15
+ end
16
+
17
+ it "wraps Postmark.send_through_postmark" do
18
+ Postmark::ApiClient.any_instance.should_receive(:deliver_message).with(message)
19
+ message.delivery_method Mail::Postmark
20
+ message.deliver
21
+ end
22
+
23
+ it "returns self by default" do
24
+ Postmark::ApiClient.any_instance.should_receive(:deliver_message).with(message)
25
+ message.delivery_method Mail::Postmark
26
+ message.deliver.should eq message
27
+ end
28
+
29
+ it "returns the actual response if :return_response setting is present" do
30
+ Postmark::ApiClient.any_instance.should_receive(:deliver_message).with(message)
31
+ message.delivery_method Mail::Postmark, :return_response => true
32
+ message.deliver.should eq message
33
+ end
34
+
35
+ it "allows to set the api key" do
36
+ message.delivery_method Mail::Postmark, {:api_key => 'api-key'}
37
+ message.delivery_method.settings[:api_key].should == 'api-key'
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe Postmark::HashHelper do
4
+ describe ".to_postmark" do
5
+ let(:source) { {:from => "support@postmarkapp.com", :reply_to => "contact@wildbit.com"} }
6
+ let(:target) { {"From" => "support@postmarkapp.com", "ReplyTo" => "contact@wildbit.com"} }
7
+
8
+ it 'converts Hash keys to Postmark format' do
9
+ subject.to_postmark(source).should == target
10
+ end
11
+
12
+ it 'acts idempotentely' do
13
+ subject.to_postmark(target).should == target
14
+ end
15
+ end
16
+
17
+ describe ".to_ruby" do
18
+ let(:source) { {"From" => "support@postmarkapp.com", "ReplyTo" => "contact@wildbit.com"} }
19
+ let(:target) { {:from => "support@postmarkapp.com", :reply_to => "contact@wildbit.com"} }
20
+
21
+ it 'converts Hash keys to Ruby format' do
22
+ subject.to_ruby(source).should == target
23
+ end
24
+
25
+ it 'has compatible mode' do
26
+ subject.to_ruby(source, true).should == target.merge(source)
27
+ end
28
+
29
+ it 'acts idempotentely' do
30
+ subject.to_ruby(target).should == target
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Postmark::MessageHelper do
4
+ let(:attachments) {
5
+ [
6
+ File.open(empty_gif_path),
7
+ {:name => "img2.gif",
8
+ :content => Postmark::MessageHelper.encode_in_base64(File.read(empty_gif_path)),
9
+ :content_type => "application/octet-stream"}
10
+ ]
11
+ }
12
+
13
+ let(:postmark_attachments) {
14
+ content = Postmark::MessageHelper.encode_in_base64(File.read(empty_gif_path))
15
+ [
16
+ {"Name" => "empty.gif",
17
+ "Content" => content,
18
+ "ContentType" => "application/octet-stream"},
19
+ {"Name" => "img2.gif",
20
+ "Content" => content,
21
+ "ContentType" => "application/octet-stream"}
22
+ ]
23
+ }
24
+
25
+ let(:headers) {
26
+ [{:name => "CUSTOM-HEADER", :value => "value"}]
27
+ }
28
+
29
+ let(:postmark_headers) {
30
+ [{"Name" => "CUSTOM-HEADER", "Value" => "value"}]
31
+ }
32
+
33
+
34
+ describe ".to_postmark" do
35
+ let(:message) {
36
+ {
37
+ :from => "sender@example.com",
38
+ :to => "receiver@example.com",
39
+ :cc => "copied@example.com",
40
+ :bcc => "blank-copied@example.com",
41
+ :subject => "Test",
42
+ :tag => "Invitation",
43
+ :html_body => "<b>Hello</b>",
44
+ :text_body => "Hello",
45
+ :reply_to => "reply@example.com"
46
+ }
47
+ }
48
+
49
+ let(:postmark_message) {
50
+ {
51
+ "From" => "sender@example.com",
52
+ "To" => "receiver@example.com",
53
+ "Cc" => "copied@example.com",
54
+ "Bcc"=> "blank-copied@example.com",
55
+ "Subject" => "Test",
56
+ "Tag" => "Invitation",
57
+ "HtmlBody" => "<b>Hello</b>",
58
+ "TextBody" => "Hello",
59
+ "ReplyTo" => "reply@example.com"
60
+ }
61
+ }
62
+
63
+ let(:message_with_headers) {
64
+ message.merge(:headers => headers)
65
+ }
66
+
67
+ let(:postmark_message_with_headers) {
68
+ postmark_message.merge("Headers" => postmark_headers)
69
+ }
70
+
71
+ let(:message_with_headers_and_attachments) {
72
+ message_with_headers.merge(:attachments => attachments)
73
+ }
74
+
75
+ let(:postmark_message_with_headers_and_attachments) {
76
+ postmark_message_with_headers.merge("Attachments" => postmark_attachments)
77
+ }
78
+
79
+ it 'converts messages without custom headers and attachments correctly' do
80
+ subject.to_postmark(message).should == postmark_message
81
+ end
82
+
83
+ it 'converts messages with custom headers and without attachments correctly' do
84
+ subject.to_postmark(message_with_headers).should == postmark_message_with_headers
85
+ end
86
+
87
+ it 'converts messages with custom headers and attachments correctly' do
88
+ subject.to_postmark(message_with_headers_and_attachments).should == postmark_message_with_headers_and_attachments
89
+ end
90
+
91
+ end
92
+
93
+ describe ".headers_to_postmark" do
94
+ it 'converts headers to Postmark format' do
95
+ subject.headers_to_postmark(headers).should == postmark_headers
96
+ end
97
+
98
+ it 'accepts single header as a non-array' do
99
+ subject.headers_to_postmark(headers.first).should == [postmark_headers.first]
100
+ end
101
+ end
102
+
103
+ describe ".attachments_to_postmark" do
104
+
105
+ it 'converts attachments to Postmark format' do
106
+ subject.attachments_to_postmark(attachments).should == postmark_attachments
107
+ end
108
+
109
+ it 'accepts single attachment as a non-array' do
110
+ subject.attachments_to_postmark(attachments.first).should == [postmark_attachments.first]
111
+ end
112
+
113
+ end
114
+
115
+ end
@@ -0,0 +1,204 @@
1
+ require 'spec_helper'
2
+
3
+ describe Postmark::HttpClient do
4
+
5
+ def response_body(status, message = "")
6
+ body = {"ErrorCode" => status, "Message" => message}.to_json
7
+ end
8
+
9
+ let(:api_key) { "provided-postmark-api-key" }
10
+ let(:secure) { true }
11
+ let(:proxy_host) { "providedproxyhostname.com" }
12
+ let(:proxy_port) { 42 }
13
+ let(:proxy_user) { "provided proxy user" }
14
+ let(:proxy_pass) { "provided proxy pass" }
15
+ let(:host) { "providedhostname.org" }
16
+ let(:port) { 443 }
17
+ let(:path_prefix) { "/provided/path/prefix" }
18
+ let(:http_open_timeout) { 42 }
19
+ let(:http_read_timeout) { 42 }
20
+
21
+ let(:http_client) { Postmark::HttpClient.new(api_key) }
22
+ subject { http_client }
23
+
24
+ context "attr writers" do
25
+ it { should respond_to(:api_key=) }
26
+ end
27
+
28
+ context "attr readers" do
29
+ it { should respond_to(:http) }
30
+ it { should respond_to(:secure) }
31
+ it { should respond_to(:api_key) }
32
+ it { should respond_to(:proxy_host) }
33
+ it { should respond_to(:proxy_port) }
34
+ it { should respond_to(:proxy_user) }
35
+ it { should respond_to(:proxy_pass) }
36
+ it { should respond_to(:host) }
37
+ it { should respond_to(:port) }
38
+ it { should respond_to(:path_prefix) }
39
+ it { should respond_to(:http_open_timeout) }
40
+ it { should respond_to(:http_read_timeout) }
41
+ end
42
+
43
+ context "when it is created without options" do
44
+ its(:api_key) { should eq api_key }
45
+ its(:host) { should eq 'api.postmarkapp.com' }
46
+ its(:port) { should eq 80 }
47
+ its(:secure) { should be_false }
48
+ its(:path_prefix) { should eq '/' }
49
+ its(:http_read_timeout) { should eq 15 }
50
+ its(:http_open_timeout) { should eq 5 }
51
+ end
52
+
53
+ context "when it is created with options" do
54
+ subject { Postmark::HttpClient.new(api_key,
55
+ :secure => secure,
56
+ :proxy_host => proxy_host,
57
+ :proxy_port => proxy_port,
58
+ :proxy_user => proxy_user,
59
+ :proxy_pass => proxy_pass,
60
+ :host => host,
61
+ :port => port,
62
+ :path_prefix => path_prefix,
63
+ :http_open_timeout => http_open_timeout,
64
+ :http_read_timeout => http_read_timeout) }
65
+
66
+ its(:api_key) { should eq api_key }
67
+ its(:secure) { should == secure }
68
+ its(:proxy_host) { should == proxy_host }
69
+ its(:proxy_port) { should == proxy_port }
70
+ its(:proxy_user) { should == proxy_user }
71
+ its(:proxy_pass) { should == proxy_pass }
72
+ its(:host) { should == host }
73
+ its(:port) { should == port }
74
+ its(:path_prefix) { should == path_prefix }
75
+ its(:http_open_timeout) { should == http_open_timeout }
76
+ its(:http_read_timeout) { should == http_read_timeout }
77
+ end
78
+
79
+ describe "#post" do
80
+ let(:target_path) { "path/on/server" }
81
+ let(:target_url) { "http://api.postmarkapp.com/#{target_path}" }
82
+
83
+ it "sends a POST request to provided URI" do
84
+ FakeWeb.register_uri(:post, target_url, :body => response_body(200))
85
+ subject.post(target_path)
86
+ FakeWeb.should have_requested(:post, target_url)
87
+ end
88
+
89
+ it "raises a custom error when API key authorization fails" do
90
+ FakeWeb.register_uri(:post, target_url, :body => response_body(401),
91
+ :status => [ "401", "Unauthorized" ])
92
+ expect { subject.post(target_path) }.to raise_error Postmark::InvalidApiKeyError
93
+ end
94
+
95
+ it "raises a custom error when sent JSON was not valid" do
96
+ FakeWeb.register_uri(:post, target_url, :body => response_body(422),
97
+ :status => [ "422", "Invalid" ])
98
+ expect { subject.post(target_path) }.to raise_error Postmark::InvalidMessageError
99
+ end
100
+
101
+ it "raises a custom error when server fails to process the request" do
102
+ FakeWeb.register_uri(:post, target_url, :body => response_body(500),
103
+ :status => [ "500", "Internal Server Error" ])
104
+ expect { subject.post(target_path) }.to raise_error Postmark::InternalServerError
105
+ end
106
+
107
+ it "raises a custom error when the request times out" do
108
+ subject.http.should_receive(:post).at_least(:once).
109
+ and_raise(Timeout::Error)
110
+ expect { subject.post(target_path) }.to raise_error Postmark::TimeoutError
111
+ end
112
+
113
+ it "raises a default error when unknown issue occurs" do
114
+ FakeWeb.register_uri(:post, target_url, :body => response_body(485),
115
+ :status => [ "485", "Custom HTTP response status" ])
116
+ expect { subject.post(target_path) }.to raise_error Postmark::UnknownError
117
+ end
118
+
119
+ end
120
+
121
+ describe "#get" do
122
+ let(:target_path) { "path/on/server" }
123
+ let(:target_url) { "http://api.postmarkapp.com/#{target_path}" }
124
+
125
+ it "sends a GET request to provided URI" do
126
+ FakeWeb.register_uri(:get, target_url, :body => response_body(200))
127
+ subject.get(target_path)
128
+ FakeWeb.should have_requested(:get, target_url)
129
+ end
130
+
131
+ it "raises a custom error when API key authorization fails" do
132
+ FakeWeb.register_uri(:get, target_url, :body => response_body(401),
133
+ :status => [ "401", "Unauthorized" ])
134
+ expect { subject.get(target_path) }.to raise_error Postmark::InvalidApiKeyError
135
+ end
136
+
137
+ it "raises a custom error when sent JSON was not valid" do
138
+ FakeWeb.register_uri(:get, target_url, :body => response_body(422),
139
+ :status => [ "422", "Invalid" ])
140
+ expect { subject.get(target_path) }.to raise_error Postmark::InvalidMessageError
141
+ end
142
+
143
+ it "raises a custom error when server fails to process the request" do
144
+ FakeWeb.register_uri(:get, target_url, :body => response_body(500),
145
+ :status => [ "500", "Internal Server Error" ])
146
+ expect { subject.get(target_path) }.to raise_error Postmark::InternalServerError
147
+ end
148
+
149
+ it "raises a custom error when the request times out" do
150
+ subject.http.should_receive(:get).at_least(:once).
151
+ and_raise(Timeout::Error)
152
+ expect { subject.get(target_path) }.to raise_error Postmark::TimeoutError
153
+ end
154
+
155
+ it "raises a default error when unknown issue occurs" do
156
+ FakeWeb.register_uri(:get, target_url, :body => response_body(485),
157
+ :status => [ "485", "Custom HTTP response status" ])
158
+ expect { subject.get(target_path) }.to raise_error Postmark::UnknownError
159
+ end
160
+
161
+ end
162
+
163
+ describe "#put" do
164
+ let(:target_path) { "path/on/server" }
165
+ let(:target_url) { "http://api.postmarkapp.com/#{target_path}" }
166
+
167
+ it "sends a PUT request to provided URI" do
168
+ FakeWeb.register_uri(:put, target_url, :body => response_body(200))
169
+ subject.put(target_path)
170
+ FakeWeb.should have_requested(:put, target_url)
171
+ end
172
+
173
+ it "raises a custom error when API key authorization fails" do
174
+ FakeWeb.register_uri(:put, target_url, :body => response_body(401),
175
+ :status => [ "401", "Unauthorized" ])
176
+ expect { subject.put(target_path) }.to raise_error Postmark::InvalidApiKeyError
177
+ end
178
+
179
+ it "raises a custom error when sent JSON was not valid" do
180
+ FakeWeb.register_uri(:put, target_url, :body => response_body(422),
181
+ :status => [ "422", "Invalid" ])
182
+ expect { subject.put(target_path) }.to raise_error Postmark::InvalidMessageError
183
+ end
184
+
185
+ it "raises a custom error when server fails to process the request" do
186
+ FakeWeb.register_uri(:put, target_url, :body => response_body(500),
187
+ :status => [ "500", "Internal Server Error" ])
188
+ expect { subject.put(target_path) }.to raise_error Postmark::InternalServerError
189
+ end
190
+
191
+ it "raises a custom error when the request times out" do
192
+ subject.http.should_receive(:put).at_least(:once).
193
+ and_raise(Timeout::Error)
194
+ expect { subject.put(target_path) }.to raise_error Postmark::TimeoutError
195
+ end
196
+
197
+ it "raises a default error when unknown issue occurs" do
198
+ FakeWeb.register_uri(:put, target_url, :body => response_body(485),
199
+ :status => [ "485", "Custom HTTP response status" ])
200
+ expect { subject.put(target_path) }.to raise_error Postmark::UnknownError
201
+ end
202
+
203
+ end
204
+ end