postmark 0.9.19 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,88 @@
1
+ require 'spec_helper'
2
+
3
+ describe Postmark::Inbound do
4
+ # http://developer.postmarkapp.com/developer-inbound-parse.html#example-hook
5
+ let(:example_inbound) { '{"From":"myUser@theirDomain.com","FromFull":{"Email":"myUser@theirDomain.com","Name":"John Doe"},"To":"451d9b70cf9364d23ff6f9d51d870251569e+ahoy@inbound.postmarkapp.com","ToFull":[{"Email":"451d9b70cf9364d23ff6f9d51d870251569e+ahoy@inbound.postmarkapp.com","Name":""}],"Cc":"\"Full name\" <sample.cc@emailDomain.com>, \"Another Cc\" <another.cc@emailDomain.com>","CcFull":[{"Email":"sample.cc@emailDomain.com","Name":"Full name"},{"Email":"another.cc@emailDomain.com","Name":"Another Cc"}],"ReplyTo":"myUsersReplyAddress@theirDomain.com","Subject":"This is an inbound message","MessageID":"22c74902-a0c1-4511-804f2-341342852c90","Date":"Thu, 5 Apr 2012 16:59:01 +0200","MailboxHash":"ahoy","TextBody":"[ASCII]","HtmlBody":"[HTML(encoded)]","Tag":"","Headers":[{"Name":"X-Spam-Checker-Version","Value":"SpamAssassin 3.3.1 (2010-03-16) onrs-ord-pm-inbound1.wildbit.com"},{"Name":"X-Spam-Status","Value":"No"},{"Name":"X-Spam-Score","Value":"-0.1"},{"Name":"X-Spam-Tests","Value":"DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,SPF_PASS"},{"Name":"Received-SPF","Value":"Pass (sender SPF authorized) identity=mailfrom; client-ip=209.85.160.180; helo=mail-gy0-f180.google.com; envelope-from=myUser@theirDomain.com; receiver=451d9b70cf9364d23ff6f9d51d870251569e+ahoy@inbound.postmarkapp.com"},{"Name":"DKIM-Signature","Value":"v=1; a=rsa-sha256; c=relaxed\/relaxed; d=wildbit.com; s=google; h=mime-version:reply-to:date:message-id:subject:from:to:cc :content-type; bh=cYr\/+oQiklaYbBJOQU3CdAnyhCTuvemrU36WT7cPNt0=; b=QsegXXbTbC4CMirl7A3VjDHyXbEsbCUTPL5vEHa7hNkkUTxXOK+dQA0JwgBHq5C+1u iuAJMz+SNBoTqEDqte2ckDvG2SeFR+Edip10p80TFGLp5RucaYvkwJTyuwsA7xd78NKT Q9ou6L1hgy\/MbKChnp2kxHOtYNOrrszY3JfQM="},{"Name":"MIME-Version","Value":"1.0"},{"Name":"Message-ID","Value":"<CAGXpo2WKfxHWZ5UFYCR3H_J9SNMG+5AXUovfEFL6DjWBJSyZaA@mail.gmail.com>"}],"Attachments":[{"Name":"myimage.png","Content":"[BASE64-ENCODED CONTENT]","ContentType":"image/png","ContentLength":4096},{"Name":"mypaper.doc","Content":"[BASE64-ENCODED CONTENT]","ContentType":"application/msword","ContentLength":16384}]}' }
6
+
7
+ context "given a serialized inbound document" do
8
+ subject { Postmark::Inbound.to_ruby_hash(example_inbound) }
9
+
10
+ it { should have_key(:from) }
11
+ it { should have_key(:from_full) }
12
+ it { should have_key(:to) }
13
+ it { should have_key(:to_full) }
14
+ it { should have_key(:cc) }
15
+ it { should have_key(:cc_full) }
16
+ it { should have_key(:reply_to) }
17
+ it { should have_key(:subject) }
18
+ it { should have_key(:message_id) }
19
+ it { should have_key(:date) }
20
+ it { should have_key(:mailbox_hash) }
21
+ it { should have_key(:text_body) }
22
+ it { should have_key(:html_body) }
23
+ it { should have_key(:tag) }
24
+ it { should have_key(:headers) }
25
+ it { should have_key(:attachments) }
26
+
27
+ context "cc" do
28
+ it 'has 2 CCs' do
29
+ subject[:cc_full].count.should == 2
30
+ end
31
+
32
+ it 'stores CCs as an array of Ruby hashes' do
33
+ cc = subject[:cc_full].last
34
+ cc.should have_key(:email)
35
+ cc.should have_key(:name)
36
+ end
37
+ end
38
+
39
+ context "to" do
40
+ it 'has 1 recipients' do
41
+ subject[:to_full].count.should == 1
42
+ end
43
+
44
+ it 'stores TOs as an array of Ruby hashes' do
45
+ cc = subject[:to_full].last
46
+ cc.should have_key(:email)
47
+ cc.should have_key(:name)
48
+ end
49
+ end
50
+
51
+ context "from" do
52
+ it 'is a hash' do
53
+ subject[:from_full].should be_a Hash
54
+ end
55
+
56
+ it 'should have all required fields' do
57
+ subject[:from_full].should have_key(:email)
58
+ subject[:from_full].should have_key(:name)
59
+ end
60
+ end
61
+
62
+ context "headers" do
63
+ it 'has 8 headers' do
64
+ subject[:headers].count.should == 8
65
+ end
66
+
67
+ it 'stores headers as an array of Ruby hashes' do
68
+ header = subject[:headers].last
69
+ header.should have_key(:name)
70
+ header.should have_key(:value)
71
+ end
72
+ end
73
+
74
+ context "attachments" do
75
+ it 'has 2 attachments' do
76
+ subject[:attachments].count.should == 2
77
+ end
78
+
79
+ it 'stores attachemnts as an array of Ruby hashes' do
80
+ attachment = subject[:attachments].last
81
+ attachment.should have_key(:name)
82
+ attachment.should have_key(:content)
83
+ attachment.should have_key(:content_type)
84
+ attachment.should have_key(:content_length)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe Postmark::Inflector do
4
+
5
+ describe ".to_postmark" do
6
+ it 'converts rubyish underscored format to camel cased symbols accepted by the Postmark API' do
7
+ subject.to_postmark(:foo_bar).should == 'FooBar'
8
+ subject.to_postmark(:_bar).should == 'Bar'
9
+ subject.to_postmark(:really_long_long_long_long_symbol).should == 'ReallyLongLongLongLongSymbol'
10
+ subject.to_postmark(:foo_bar_1).should == 'FooBar1'
11
+ end
12
+
13
+ it 'accepts strings as well' do
14
+ subject.to_postmark('foo_bar').should == 'FooBar'
15
+ end
16
+
17
+ it 'acts idempotentely' do
18
+ subject.to_postmark('FooBar').should == 'FooBar'
19
+ end
20
+ end
21
+
22
+ describe ".to_ruby" do
23
+ it 'converts camel cased symbols returned by the Postmark API to underscored Ruby symbols' do
24
+ subject.to_ruby('FooBar').should == :foo_bar
25
+ subject.to_ruby('LongTimeAgoInAFarFarGalaxy').should == :long_time_ago_in_a_far_far_galaxy
26
+ subject.to_ruby('MessageID').should == :message_id
27
+ end
28
+
29
+ it 'acts idempotentely' do
30
+ subject.to_ruby(:foo_bar).should == :foo_bar
31
+ subject.to_ruby(:foo_bar_1).should == :foo_bar_1
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Postmark::Json do
4
+ let(:data) { {"bar" => "foo", "foo" => "bar"} }
5
+
6
+ shared_examples "json parser" do
7
+ it 'encodes and decodes data correctly' do
8
+ hash = Postmark::Json.decode(Postmark::Json.encode(data))
9
+ hash.should have_key("bar")
10
+ hash.should have_key("foo")
11
+ end
12
+ end
13
+
14
+ context "given response parser is JSON" do
15
+ before do
16
+ Postmark.response_parser_class = :Json
17
+ end
18
+
19
+ it_behaves_like "json parser"
20
+ end
21
+
22
+ context "given response parser is ActiveSupport::JSON" do
23
+ before do
24
+ Postmark.response_parser_class = :ActiveSupport
25
+ end
26
+
27
+ it_behaves_like "json parser"
28
+ end
29
+
30
+ context "given response parser is Yajl", :skip_for_platform => 'java' do
31
+ before do
32
+ Postmark.response_parser_class = :Yajl
33
+ end
34
+
35
+ it_behaves_like "json parser"
36
+ end
37
+ end
@@ -0,0 +1,205 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mail::Message do
4
+ before do
5
+ Kernel.stub(:warn)
6
+ end
7
+
8
+ let(:mail_message) do
9
+ Mail.new do
10
+ from "sheldon@bigbangtheory.com"
11
+ to "lenard@bigbangtheory.com"
12
+ subject "Hello!"
13
+ body "Hello Sheldon!"
14
+ end
15
+ end
16
+
17
+ let(:tagged_mail_message) do
18
+ Mail.new do
19
+ from "sheldon@bigbangtheory.com"
20
+ to "lenard@bigbangtheory.com"
21
+ subject "Hello!"
22
+ body "Hello Sheldon!"
23
+ tag "sheldon"
24
+ end
25
+ end
26
+
27
+ let(:mail_message_without_body) do
28
+ Mail.new do
29
+ from "sheldon@bigbangtheory.com"
30
+ to "lenard@bigbangtheory.com"
31
+ subject "Hello!"
32
+ end
33
+ end
34
+
35
+ let(:mail_html_message) do
36
+ mail = Mail.new do
37
+ from "sheldon@bigbangtheory.com"
38
+ to "lenard@bigbangtheory.com"
39
+ subject "Hello!"
40
+ content_type 'text/html; charset=UTF-8'
41
+ body "<b>Hello Sheldon!</b>"
42
+ end
43
+ end
44
+
45
+ let(:mail_multipart_message) do
46
+ mail = Mail.new do
47
+ from "sheldon@bigbangtheory.com"
48
+ to "lenard@bigbangtheory.com"
49
+ subject "Hello!"
50
+ text_part do
51
+ body "Hello Sheldon!"
52
+ end
53
+ html_part do
54
+ body "<b>Hello Sheldon!</b>"
55
+ end
56
+ end
57
+ end
58
+
59
+ let(:mail_message_with_attachment) do
60
+ Mail.new do
61
+ from "sheldon@bigbangtheory.com"
62
+ to "lenard@bigbangtheory.com"
63
+ subject "Hello!"
64
+ body "Hello Sheldon!"
65
+ add_file empty_gif_path
66
+ end
67
+ end
68
+
69
+ describe "#html?" do
70
+ it 'is true for html only email' do
71
+ mail_html_message.should be_html
72
+ end
73
+ end
74
+
75
+ describe "#body_html" do
76
+ it 'returns html body if present' do
77
+ mail_html_message.body_html.should == "<b>Hello Sheldon!</b>"
78
+ end
79
+ end
80
+
81
+ describe "#body_text" do
82
+ it 'returns text body if present' do
83
+ mail_message.body_text.should == "Hello Sheldon!"
84
+ end
85
+ end
86
+
87
+ describe "#postmark_attachments=" do
88
+ let(:attached_hash) { {'Name' => 'picture.jpeg',
89
+ 'ContentType' => 'image/jpeg'} }
90
+
91
+ it "stores attachments as an array" do
92
+ mail_message.postmark_attachments = attached_hash
93
+ mail_message.instance_variable_get(:@_attachments).should include(attached_hash)
94
+ end
95
+
96
+ it "is deprecated" do
97
+ Kernel.should_receive(:warn).with(/deprecated/)
98
+ mail_message.postmark_attachments = attached_hash
99
+ end
100
+ end
101
+
102
+ describe "#postmark_attachments" do
103
+ let(:attached_file) { mock("file") }
104
+ let(:attached_hash) { {'Name' => 'picture.jpeg',
105
+ 'ContentType' => 'image/jpeg'} }
106
+ let(:exported_file) { {'Name' => 'file.jpeg',
107
+ 'ContentType' => 'application/octet-stream',
108
+ 'Content' => ''} }
109
+
110
+ before do
111
+ attached_file.stub(:is_a?) { |arg| arg == File ? true : false }
112
+ attached_file.stub(:path) { '/tmp/file.jpeg' }
113
+ end
114
+
115
+ it "supports multiple attachment formats" do
116
+ IO.should_receive(:read).with("/tmp/file.jpeg").and_return("")
117
+
118
+ mail_message.postmark_attachments = [attached_hash, attached_file]
119
+ attachments = mail_message.export_attachments
120
+
121
+ attachments.should include(attached_hash)
122
+ attachments.should include(exported_file)
123
+ end
124
+
125
+ it "is deprecated" do
126
+ mail_message.postmark_attachments = attached_hash
127
+ Kernel.should_receive(:warn).with(/deprecated/)
128
+ mail_message.postmark_attachments
129
+ end
130
+ end
131
+
132
+ describe "#export_attachments" do
133
+ let(:file_data) { 'binarydatahere' }
134
+ let(:exported_data) do
135
+ {'Name' => 'face.jpeg',
136
+ 'Content' => "YmluYXJ5ZGF0YWhlcmU=\n",
137
+ 'ContentType' => 'image/jpeg'}
138
+ end
139
+
140
+ it "exports native attachments" do
141
+ mail_message.attachments["face.jpeg"] = file_data
142
+ mail_message.export_attachments.should include(exported_data)
143
+ end
144
+
145
+ it "still supports the deprecated attachments API" do
146
+ mail_message.attachments["face.jpeg"] = file_data
147
+ mail_message.postmark_attachments = exported_data
148
+ mail_message.export_attachments.should == [exported_data, exported_data]
149
+ end
150
+ end
151
+
152
+ describe "#to_postmark_hash" do
153
+ it 'converts plain text messages correctly' do
154
+ mail_message.to_postmark_hash.should == {
155
+ "From" => "sheldon@bigbangtheory.com",
156
+ "Subject" => "Hello!",
157
+ "TextBody" => "Hello Sheldon!",
158
+ "To" => "lenard@bigbangtheory.com"}
159
+ end
160
+
161
+ it 'converts tagged text messages correctly' do
162
+ tagged_mail_message.to_postmark_hash.should == {
163
+ "From" => "sheldon@bigbangtheory.com",
164
+ "Subject" => "Hello!",
165
+ "TextBody" => "Hello Sheldon!",
166
+ "Tag" => "sheldon",
167
+ "To"=>"lenard@bigbangtheory.com"}
168
+ end
169
+
170
+ it 'converts plain text messages without body correctly' do
171
+ mail_message_without_body.to_postmark_hash.should == {
172
+ "From" => "sheldon@bigbangtheory.com",
173
+ "Subject" => "Hello!",
174
+ "To" => "lenard@bigbangtheory.com"}
175
+ end
176
+
177
+ it 'converts html messages correctly' do
178
+ mail_html_message.to_postmark_hash.should == {
179
+ "From" => "sheldon@bigbangtheory.com",
180
+ "Subject" => "Hello!",
181
+ "HtmlBody" => "<b>Hello Sheldon!</b>",
182
+ "To" => "lenard@bigbangtheory.com"}
183
+ end
184
+
185
+ it 'converts multipart messages correctly' do
186
+ mail_multipart_message.to_postmark_hash.should == {
187
+ "From" => "sheldon@bigbangtheory.com",
188
+ "Subject" => "Hello!",
189
+ "HtmlBody" => "<b>Hello Sheldon!</b>",
190
+ "TextBody" => "Hello Sheldon!",
191
+ "To" => "lenard@bigbangtheory.com"}
192
+ end
193
+
194
+ it 'converts messages with attachments correctly' do
195
+ mail_message_with_attachment.to_postmark_hash.should == {
196
+ "From" => "sheldon@bigbangtheory.com",
197
+ "Subject" => "Hello!",
198
+ "Attachments" => [{"Name"=>"empty.gif",
199
+ "Content"=>encoded_empty_gif_data,
200
+ "ContentType"=>"image/gif"}],
201
+ "TextBody"=>"Hello Sheldon!",
202
+ "To"=>"lenard@bigbangtheory.com"}
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,164 @@
1
+ require 'spec_helper'
2
+
3
+ describe Postmark do
4
+ let(:api_key) { mock }
5
+ let(:secure) { mock }
6
+ let(:proxy_host) { mock }
7
+ let(:proxy_port) { mock }
8
+ let(:proxy_user) { mock }
9
+ let(:proxy_pass) { mock }
10
+ let(:host) { mock }
11
+ let(:port) { mock }
12
+ let(:path_prefix) { mock }
13
+ let(:max_retries) { mock }
14
+
15
+ before do
16
+ subject.api_key = api_key
17
+ subject.secure = secure
18
+ subject.proxy_host = proxy_host
19
+ subject.proxy_port = proxy_port
20
+ subject.proxy_user = proxy_user
21
+ subject.proxy_pass = proxy_pass
22
+ subject.host = host
23
+ subject.port = port
24
+ subject.path_prefix = path_prefix
25
+ subject.max_retries = max_retries
26
+ end
27
+
28
+ context "attr readers" do
29
+ it { should respond_to(:secure) }
30
+ it { should respond_to(:api_key) }
31
+ it { should respond_to(:proxy_host) }
32
+ it { should respond_to(:proxy_port) }
33
+ it { should respond_to(:proxy_user) }
34
+ it { should respond_to(:proxy_pass) }
35
+ it { should respond_to(:host) }
36
+ it { should respond_to(:port) }
37
+ it { should respond_to(:path_prefix) }
38
+ it { should respond_to(:http_open_timeout) }
39
+ it { should respond_to(:http_read_timeout) }
40
+ it { should respond_to(:max_retries) }
41
+ end
42
+
43
+ context "attr writers" do
44
+ it { should respond_to(:secure=) }
45
+ it { should respond_to(:api_key=) }
46
+ it { should respond_to(:proxy_host=) }
47
+ it { should respond_to(:proxy_port=) }
48
+ it { should respond_to(:proxy_user=) }
49
+ it { should respond_to(:proxy_pass=) }
50
+ it { should respond_to(:host=) }
51
+ it { should respond_to(:port=) }
52
+ it { should respond_to(:path_prefix=) }
53
+ it { should respond_to(:http_open_timeout=) }
54
+ it { should respond_to(:http_read_timeout=) }
55
+ it { should respond_to(:max_retries=) }
56
+ it { should respond_to(:response_parser_class=) }
57
+ it { should respond_to(:api_client=) }
58
+ end
59
+
60
+ describe ".response_parser_class" do
61
+
62
+ after do
63
+ subject.instance_variable_set(:@response_parser_class, nil)
64
+ end
65
+
66
+ it "returns :ActiveSupport when ActiveSupport::JSON is available" do
67
+ subject.response_parser_class.should == :ActiveSupport
68
+ end
69
+
70
+ it "returns :Json when ActiveSupport::JSON is not available" do
71
+ hide_const("ActiveSupport::JSON")
72
+ subject.response_parser_class.should == :Json
73
+ end
74
+
75
+ end
76
+
77
+ describe ".configure" do
78
+
79
+ it 'yields itself to the block' do
80
+ expect { |b| subject.configure(&b) }.to yield_with_args(subject)
81
+ end
82
+
83
+ end
84
+
85
+ describe ".api_client" do
86
+ let(:api_client) { mock }
87
+
88
+ context "when shared client instance already exists" do
89
+
90
+ it 'returns the existing instance' do
91
+ subject.instance_variable_set(:@api_client, api_client)
92
+ subject.api_client.should == api_client
93
+ end
94
+
95
+ end
96
+
97
+ context "when shared client instance does not exist" do
98
+
99
+ it 'creates a new instance of Postmark::ApiClient' do
100
+ Postmark::ApiClient.should_receive(:new).
101
+ with(api_key,
102
+ :secure => secure,
103
+ :proxy_host => proxy_host,
104
+ :proxy_port => proxy_port,
105
+ :proxy_user => proxy_user,
106
+ :proxy_pass => proxy_pass,
107
+ :host => host,
108
+ :port => port,
109
+ :path_prefix => path_prefix,
110
+ :max_retries => max_retries).
111
+ and_return(api_client)
112
+ subject.api_client.should == api_client
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+
119
+ describe ".deliver_message" do
120
+ let(:api_client) { mock }
121
+ let(:message) { mock }
122
+
123
+ before do
124
+ subject.api_client = api_client
125
+ end
126
+
127
+ it 'delegates the method to the shared api client instance' do
128
+ api_client.should_receive(:deliver_message).with(message)
129
+ subject.deliver_message(message)
130
+ end
131
+
132
+ it 'is also accessible as .send_through_postmark' do
133
+ api_client.should_receive(:deliver_message).with(message)
134
+ subject.send_through_postmark(message)
135
+ end
136
+ end
137
+
138
+ describe ".deliver_messages" do
139
+ let(:api_client) { mock }
140
+ let(:message) { mock }
141
+
142
+ before do
143
+ subject.api_client = api_client
144
+ end
145
+
146
+ it 'delegates the method to the shared api client instance' do
147
+ api_client.should_receive(:deliver_messages).with(message)
148
+ subject.deliver_messages(message)
149
+ end
150
+ end
151
+
152
+ describe ".delivery_stats" do
153
+ let(:api_client) { mock }
154
+
155
+ before do
156
+ subject.api_client = api_client
157
+ end
158
+
159
+ it 'delegates the method to the shared api client instance' do
160
+ api_client.should_receive(:delivery_stats)
161
+ subject.delivery_stats
162
+ end
163
+ end
164
+ end