postmark 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -22,6 +22,7 @@ Then
22
22
 
23
23
  message = TMail::Mail.new
24
24
  # make sure you have a sender signature with that email
25
+ # from and to also accept arrays of emails.
25
26
  message.from = "leonard@bigbangtheory.com"
26
27
  message.to = "Sheldon Cooper <sheldon@bigbangtheory.com>"
27
28
  message.subject = "Hi Sheldon!"
@@ -29,11 +30,29 @@ Then
29
30
  message.body = "Hello my friend!"
30
31
  # set custom headers at will.
31
32
  message["CUSTOM-HEADER"] = "my custom header value"
33
+ # tag message
34
+ message.tag = "my-tracking-tag"
32
35
  # set reply to if you need; also, you can pass array of emails.
33
36
  message.reply_to = "penny@bigbangtheory.com"
34
37
 
35
38
  p Postmark.send_through_postmark(message).body
36
39
 
40
+ You can retrieve various information about your server state using the "Public bounces API":http://developer.postmarkapp.com/bounces
41
+
42
+ # get delivery stats
43
+ Postmark.delivery_stats # json
44
+ Postmark::Bounce.all # [ bounce, bounce ... ]
45
+ bounce = Postmark::Bounce.find(bounce_id) # Bounce
46
+ bounce.dump # string, containing raw SMTP data
47
+ bounce.reactivate # reactivate hard bounce
48
+
49
+ == Encryption
50
+
51
+ To use SSL encryption when sending email configure the library as follows:
52
+
53
+ Postmark.secure = true
54
+
55
+
37
56
  == Requirements
38
57
 
39
58
  The gem relies on TMail for building the message. You will also need postmark account, server and sender signature set up to use it.
@@ -46,7 +65,7 @@ You can also explicitly specify which one to be used, using
46
65
 
47
66
  == Limitations
48
67
 
49
- Currently postmark API does not support multiple recipients, or attachments. For more information, check the docs at:
68
+ Currently postmark API does not support attachments. For more information, check the docs at:
50
69
 
51
70
  http://developer.postmarkapp.com
52
71
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.0
1
+ 0.7.0
data/lib/postmark.rb CHANGED
@@ -3,6 +3,9 @@ require 'net/https'
3
3
  require 'rubygems'
4
4
  require 'tmail'
5
5
  require 'postmark/tmail_mail_extension'
6
+ require 'postmark/bounce'
7
+ require 'postmark/json'
8
+ require 'postmark/http_client'
6
9
 
7
10
  module Postmark
8
11
 
@@ -25,7 +28,7 @@ module Postmark
25
28
  MAX_RETRIES = 2
26
29
 
27
30
  class << self
28
- attr_accessor :host, :host_path, :port, :secure, :api_key, :http_open_timeout, :http_read_timeout,
31
+ attr_accessor :host, :path_prefix, :port, :secure, :api_key, :http_open_timeout, :http_read_timeout,
29
32
  :proxy_host, :proxy_port, :proxy_user, :proxy_pass, :max_retries, :sleep_between_retries
30
33
 
31
34
  attr_writer :response_parser_class
@@ -45,8 +48,8 @@ module Postmark
45
48
  end
46
49
 
47
50
  # The path of the listener
48
- def host_path
49
- @host_path ||= 'email'
51
+ def path_prefix
52
+ @path_prefix ||= '/'
50
53
  end
51
54
 
52
55
  def http_open_timeout
@@ -69,18 +72,10 @@ module Postmark
69
72
  yield self
70
73
  end
71
74
 
72
- def protocol #:nodoc:
73
- secure ? "https" : "http"
74
- end
75
-
76
- def url #:nodoc:
77
- URI.parse("#{protocol}://#{host}:#{port}/#{host_path}/")
78
- end
79
-
80
75
  def send_through_postmark(message) #:nodoc:
81
76
  @retries = 0
82
77
  begin
83
- attempt_sending(message)
78
+ HttpClient.post("email", Postmark::Json.encode(convert_tmail(message)))
84
79
  rescue Exception => e
85
80
  if @retries < max_retries
86
81
  @retries += 1
@@ -91,46 +86,11 @@ module Postmark
91
86
  end
92
87
  end
93
88
 
94
- def attempt_sending(message)
95
- ResponseParsers.const_get(response_parser_class) # loads JSON lib, defining #to_json
96
- http = Net::HTTP::Proxy(proxy_host,
97
- proxy_port,
98
- proxy_user,
99
- proxy_pass).new(url.host, url.port)
100
-
101
- http.read_timeout = http_read_timeout
102
- http.open_timeout = http_open_timeout
103
- http.use_ssl = !!secure
104
-
105
- headers = HEADERS.merge({ "X-Postmark-Server-Token" => api_key.to_s })
106
-
107
- response = http.post(url.path, convert_tmail(message).to_json, headers)
108
-
109
- case response.code.to_i
110
- when 200
111
- return response
112
- when 401
113
- raise InvalidApiKeyError, error_message(response.body)
114
- when 422
115
- raise InvalidMessageError, error_message(response.body)
116
- when 500
117
- raise InternalServerError, response.body
118
- else
119
- raise UnknownError, response
120
- end
89
+ def delivery_stats
90
+ HttpClient.get("deliverystats")
121
91
  end
122
92
 
123
- def error_message(response_body)
124
- decode_json(response_body)["Message"]
125
- end
126
-
127
- def decode_json(data)
128
- ResponseParsers.const_get(response_parser_class).decode(data)
129
- end
130
-
131
- def encode_json(data)
132
- ResponseParsers.const_get(response_parser_class).encode(data)
133
- end
93
+ protected
134
94
 
135
95
  def convert_tmail(message)
136
96
  options = { "From" => message['from'].to_s, "To" => message['to'].to_s, "Subject" => message.subject }
@@ -0,0 +1,56 @@
1
+ module Postmark
2
+ class Bounce
3
+
4
+ attr_reader :email, :bounced_at, :type, :details, :name, :id, :server_id, :tag
5
+
6
+ def initialize(values = {})
7
+ @id = values["ID"]
8
+ @email = values["Email"]
9
+ @bounced_at = Time.parse(values["BouncedAt"])
10
+ @type = values["Type"]
11
+ @name = values["Name"]
12
+ @details = values["Details"]
13
+ @tag = values["Tag"]
14
+ @dump_available = values["DumpAvailable"]
15
+ @inactive = values["Inactive"]
16
+ @can_activate = values["CanActivate"]
17
+ end
18
+
19
+ def inactive?
20
+ !!@inactive
21
+ end
22
+
23
+ def can_activate?
24
+ !!@can_activate
25
+ end
26
+
27
+ def dump
28
+ Postmark::HttpClient.get("bounces/#{id}/dump")["Body"]
29
+ end
30
+
31
+ def activate
32
+ Bounce.new(Postmark::HttpClient.put("bounces/#{id}/activate")["Bounce"])
33
+ end
34
+
35
+ def dump_available?
36
+ !!@dump_available
37
+ end
38
+
39
+ class << self
40
+ def find(id)
41
+ Bounce.new(Postmark::HttpClient.get("bounces/#{id}"))
42
+ end
43
+
44
+ def all(options = {})
45
+ options[:count] ||= 30
46
+ options[:offset] ||= 0
47
+ Postmark::HttpClient.get("bounces", options).map { |bounce_json| Bounce.new(bounce_json) }
48
+ end
49
+
50
+ def tags
51
+ Postmark::HttpClient.get("bounces/tags")
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,77 @@
1
+ require 'cgi'
2
+
3
+ module Postmark
4
+ module HttpClient
5
+ class << self
6
+ def post(path, data = '')
7
+ handle_response(http.post(url_path(path), data, headers))
8
+ end
9
+
10
+ def put(path, data = '')
11
+ handle_response(http.put(url_path(path), data, headers))
12
+ end
13
+
14
+ def get(path, query = {})
15
+ handle_response(http.get(url_path(path + to_query_string(query)), headers))
16
+ end
17
+
18
+ protected
19
+
20
+ def to_query_string(hash)
21
+ return "" if hash.empty?
22
+ "?" + hash.map { |key, value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" }.join("&")
23
+ end
24
+
25
+ def protocol
26
+ Postmark.secure ? "https" : "http"
27
+ end
28
+
29
+ def url
30
+ URI.parse("#{protocol}://#{Postmark.host}:#{Postmark.port}/")
31
+ end
32
+
33
+ def handle_response(response)
34
+ case response.code.to_i
35
+ when 200
36
+ return Postmark::Json.decode(response.body)
37
+ when 401
38
+ raise InvalidApiKeyError, error_message(response.body)
39
+ when 422
40
+ raise InvalidMessageError, error_message(response.body)
41
+ when 500
42
+ raise InternalServerError, response.body
43
+ else
44
+ raise UnknownError, response
45
+ end
46
+ end
47
+
48
+ def headers
49
+ @headers ||= HEADERS.merge({ "X-Postmark-Server-Token" => Postmark.api_key.to_s })
50
+ end
51
+
52
+ def url_path(path)
53
+ Postmark.path_prefix + path
54
+ end
55
+
56
+ def http
57
+ @http ||= build_http
58
+ end
59
+
60
+ def build_http
61
+ http = Net::HTTP::Proxy(Postmark.proxy_host,
62
+ Postmark.proxy_port,
63
+ Postmark.proxy_user,
64
+ Postmark.proxy_pass).new(url.host, url.port)
65
+
66
+ http.read_timeout = Postmark.http_read_timeout
67
+ http.open_timeout = Postmark.http_open_timeout
68
+ http.use_ssl = !!Postmark.secure
69
+ http
70
+ end
71
+
72
+ def error_message(response_body)
73
+ Postmark::Json.decode(response_body)["Message"]
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,22 @@
1
+ module Postmark
2
+ module Json
3
+
4
+ class << self
5
+ def encode(data)
6
+ json_parser
7
+ data.to_json
8
+ end
9
+
10
+ def decode(data)
11
+ json_parser.decode(data)
12
+ end
13
+
14
+ private
15
+
16
+ def json_parser
17
+ ResponseParsers.const_get(Postmark.response_parser_class)
18
+ end
19
+
20
+ end
21
+ end
22
+ end
data/postmark.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{postmark}
8
- s.version = "0.6.0"
8
+ s.version = "0.7.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Petyo Ivanov"]
12
- s.date = %q{2010-03-18}
12
+ s.date = %q{2010-04-12}
13
13
  s.description = %q{Ruby gem for sending emails through http://postmarkapp.com HTTP API. It relieas on TMail::Mail for message construction.}
14
14
  s.email = %q{underlog@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -28,11 +28,15 @@ Gem::Specification.new do |s|
28
28
  "features/step_definitions/postmark_steps.rb",
29
29
  "features/support/env.rb",
30
30
  "lib/postmark.rb",
31
+ "lib/postmark/bounce.rb",
32
+ "lib/postmark/http_client.rb",
33
+ "lib/postmark/json.rb",
31
34
  "lib/postmark/response_parsers/active_support.rb",
32
35
  "lib/postmark/response_parsers/json.rb",
33
36
  "lib/postmark/response_parsers/yajl.rb",
34
37
  "lib/postmark/tmail_mail_extension.rb",
35
38
  "postmark.gemspec",
39
+ "spec/bounce_spec.rb",
36
40
  "spec/postmark_spec.rb",
37
41
  "spec/spec.opts",
38
42
  "spec/spec_helper.rb"
@@ -49,7 +53,8 @@ Gem::Specification.new do |s|
49
53
  s.rubygems_version = %q{1.3.6}
50
54
  s.summary = %q{Ruby gem for sending emails through http://postmarkapp.com HTTP API}
51
55
  s.test_files = [
52
- "spec/postmark_spec.rb",
56
+ "spec/bounce_spec.rb",
57
+ "spec/postmark_spec.rb",
53
58
  "spec/spec_helper.rb"
54
59
  ]
55
60
 
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Bounce" do
4
+ let(:bounce_json) { %{{"Type":"HardBounce","TypeCode":1,"Details":"test bounce","Email":"jim@test.com","BouncedAt":"#{Time.now.to_s}","DumpAvailable":true,"Inactive":false,"CanActivate":true,"ID":12}} }
5
+ let(:bounces_json) { "[#{bounce_json},#{bounce_json}]" }
6
+
7
+ context "single bounce" do
8
+ let(:bounce) { Postmark::Bounce.find(12) }
9
+
10
+ before do
11
+ Timecop.freeze
12
+ FakeWeb.register_uri(:get, "http://api.postmarkapp.com/bounces/12", { :body => bounce_json })
13
+ end
14
+
15
+ after do
16
+ Timecop.return
17
+ end
18
+
19
+ it "should retrieve and parce bounce correctly" do
20
+ bounce.type.should == "HardBounce"
21
+ bounce.bounced_at.should == Time.now
22
+ bounce.details.should == "test bounce"
23
+ bounce.email.should == "jim@test.com"
24
+ end
25
+
26
+ it "should retrieve bounce dump" do
27
+ FakeWeb.register_uri(:get, "http://api.postmarkapp.com/bounces/12/dump", { :body => %{{"Body": "Some SMTP gibberish"}} } )
28
+ bounce.dump.should == "Some SMTP gibberish"
29
+ end
30
+
31
+ it "should activate inactive bounce" do
32
+ FakeWeb.register_uri(:put, "http://api.postmarkapp.com/bounces/12/activate", { :body => %{{"Message":"OK","Bounce":#{bounce_json}}} } )
33
+ bounce.activate.should be_a(Postmark::Bounce)
34
+ end
35
+
36
+ end
37
+
38
+ it "should retrieve bounces" do
39
+ FakeWeb.register_uri(:get, "http://api.postmarkapp.com/bounces?count=30&offset=0", { :body => bounces_json })
40
+ bounces = Postmark::Bounce.all
41
+ bounces.should have(2).entries
42
+ bounces[0].should be_a(Postmark::Bounce)
43
+ end
44
+
45
+ it "should retrieve bounce tags" do
46
+ FakeWeb.register_uri(:get, "http://api.postmarkapp.com/bounces/tags", { :body => '["Signup","Commit Notification"]' })
47
+ tags = Postmark::Bounce.tags
48
+ tags.should have(2).entries
49
+ tags.first.should == "Signup"
50
+ end
51
+ end
@@ -1,4 +1,4 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
1
+ require 'spec_helper'
2
2
 
3
3
  describe "Postmark" do
4
4
 
@@ -23,38 +23,38 @@ describe "Postmark" do
23
23
 
24
24
  context "service call" do
25
25
 
26
- before do
26
+ before(:all) do
27
27
  Postmark.sleep_between_retries = 0
28
28
  end
29
29
 
30
30
  it "should send email successfully" do
31
- FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email/", {})
31
+ FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email", {})
32
32
  Postmark.send_through_postmark(message)
33
- FakeWeb.should have_requested(:post, "http://api.postmarkapp.com/email/")
33
+ FakeWeb.should have_requested(:post, "http://api.postmarkapp.com/email")
34
34
  end
35
35
 
36
36
  it "should warn when header is invalid" do
37
- FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email/", {:status => [ "401", "Unauthorized" ], :body => "Missing API token"})
37
+ FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email", {:status => [ "401", "Unauthorized" ], :body => "Missing API token"})
38
38
  lambda { Postmark.send_through_postmark(message) }.should raise_error(Postmark::InvalidApiKeyError)
39
39
  end
40
40
 
41
41
  it "should warn when json is not ok" do
42
- FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email/", {:status => [ "422", "Invalid" ], :body => "Invalid JSON"})
42
+ FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email", {:status => [ "422", "Invalid" ], :body => "Invalid JSON"})
43
43
  lambda { Postmark.send_through_postmark(message) }.should raise_error(Postmark::InvalidMessageError)
44
44
  end
45
45
 
46
46
  it "should warn when server fails" do
47
- FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email/", {:status => [ "500", "Internal Server Error" ]})
47
+ FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email", {:status => [ "500", "Internal Server Error" ]})
48
48
  lambda { Postmark.send_through_postmark(message) }.should raise_error(Postmark::InternalServerError)
49
49
  end
50
50
 
51
51
  it "should warn when unknown stuff fails" do
52
- FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email/", {:status => [ "485", "Custom HTTP response status" ]})
52
+ FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email", {:status => [ "485", "Custom HTTP response status" ]})
53
53
  lambda { Postmark.send_through_postmark(message) }.should raise_error(Postmark::UnknownError)
54
54
  end
55
55
 
56
56
  it "should retry 3 times" do
57
- FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email/",
57
+ FakeWeb.register_uri(:post, "http://api.postmarkapp.com/email",
58
58
  [ { :status => [ 500, "Internal Server Error" ] },
59
59
  { :status => [ 500, "Internal Server Error" ] },
60
60
  { } ]
@@ -63,19 +63,32 @@ describe "Postmark" do
63
63
  end
64
64
  end
65
65
 
66
+ context "delivery stats" do
67
+ let(:response_body) { %{{"InactiveMails":1,"Bounces":[{"TypeCode":0,"Name":"All","Count":2},{"Type":"HardBounce","TypeCode":1,"Name":"Hard bounce","Count":1},{"Type":"SoftBounce","TypeCode":4096,"Name":"Soft bounce","Count":1}]}} }
68
+
69
+ it "should query the service for delivery stats" do
70
+ FakeWeb.register_uri(:get, "http://api.postmarkapp.com/deliverystats", { :body => response_body })
71
+ results = Postmark.delivery_stats
72
+ results["InactiveMails"].should == 1
73
+ results["Bounces"].should be_an(Array)
74
+ results["Bounces"].should have(3).entries
75
+ FakeWeb.should have_requested(:get, "http://api.postmarkapp.com/deliverystats")
76
+ end
77
+ end
78
+
66
79
  context "tmail parse" do
67
80
  it "should set text body for plain message" do
68
- Postmark.convert_tmail(message)['TextBody'].should_not be_nil
81
+ Postmark.send(:convert_tmail, message)['TextBody'].should_not be_nil
69
82
  end
70
83
 
71
84
  it "should set html body for html message" do
72
- Postmark.convert_tmail(html_message)['HtmlBody'].should_not be_nil
85
+ Postmark.send(:convert_tmail, html_message)['HtmlBody'].should_not be_nil
73
86
  end
74
87
  end
75
88
 
76
89
  def be_serialized_to(json)
77
90
  simple_matcher "be serialized to #{json}" do |message|
78
- Postmark.convert_tmail(message).should == JSON.parse(json)
91
+ Postmark.send(:convert_tmail, message).should == JSON.parse(json)
79
92
  end
80
93
  end
81
94
 
@@ -111,7 +124,7 @@ describe "Postmark" do
111
124
 
112
125
  it "decodes json with #{lib}" do
113
126
  Postmark.response_parser_class = lib
114
- Postmark.decode_json(%({"Message":"OK"})).should == { "Message" => "OK" }
127
+ Postmark::Json.decode(%({"Message":"OK"})).should == { "Message" => "OK" }
115
128
  end
116
129
 
117
130
  Postmark.response_parser_class = original_parser_class
data/spec/spec_helper.rb CHANGED
@@ -7,6 +7,7 @@ require 'json'
7
7
  require 'ruby-debug'
8
8
  require 'fakeweb'
9
9
  require 'fakeweb_matcher'
10
+ require 'timecop'
10
11
  require 'spec'
11
12
  require 'spec/autorun'
12
13
 
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 6
7
+ - 7
8
8
  - 0
9
- version: 0.6.0
9
+ version: 0.7.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Petyo Ivanov
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-03-18 00:00:00 +02:00
17
+ date: 2010-04-12 00:00:00 +03:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -74,11 +74,15 @@ files:
74
74
  - features/step_definitions/postmark_steps.rb
75
75
  - features/support/env.rb
76
76
  - lib/postmark.rb
77
+ - lib/postmark/bounce.rb
78
+ - lib/postmark/http_client.rb
79
+ - lib/postmark/json.rb
77
80
  - lib/postmark/response_parsers/active_support.rb
78
81
  - lib/postmark/response_parsers/json.rb
79
82
  - lib/postmark/response_parsers/yajl.rb
80
83
  - lib/postmark/tmail_mail_extension.rb
81
84
  - postmark.gemspec
85
+ - spec/bounce_spec.rb
82
86
  - spec/postmark_spec.rb
83
87
  - spec/spec.opts
84
88
  - spec/spec_helper.rb
@@ -113,5 +117,6 @@ signing_key:
113
117
  specification_version: 3
114
118
  summary: Ruby gem for sending emails through http://postmarkapp.com HTTP API
115
119
  test_files:
120
+ - spec/bounce_spec.rb
116
121
  - spec/postmark_spec.rb
117
122
  - spec/spec_helper.rb