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,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