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,35 @@
1
+ module Postmark
2
+ module HashHelper
3
+
4
+ extend self
5
+
6
+ def to_postmark(hash)
7
+ hash.inject({}) { |m, (k,v)| m[Inflector.to_postmark(k)] = v; m }
8
+ end
9
+
10
+ def to_ruby(hash, compatible = false)
11
+ formatted = hash.inject({}) { |m, (k,v)| m[Inflector.to_ruby(k)] = v; m }
12
+
13
+ if compatible
14
+ formatted.merge!(hash)
15
+ enhance_with_compatibility_warning(formatted)
16
+ end
17
+
18
+ formatted
19
+ end
20
+
21
+ protected
22
+
23
+ def enhance_with_compatibility_warning(hash)
24
+ def hash.[](key)
25
+ if key.is_a? String
26
+ Kernel.warn("Postmark: the CamelCased String keys of response are " \
27
+ "deprecated in favor of underscored symbols. The " \
28
+ "support will be dropped in the future.")
29
+ end
30
+ super
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ module Postmark
2
+ module MessageHelper
3
+
4
+ extend self
5
+
6
+ def to_postmark(message = {})
7
+ message = message.dup
8
+
9
+ %w(to reply_to cc bcc).each do |field|
10
+ message[field.to_sym] = Array[*message[field.to_sym]].join(", ")
11
+ end
12
+
13
+ if message[:headers]
14
+ message[:headers] = headers_to_postmark(message[:headers])
15
+ end
16
+
17
+ if message[:attachments]
18
+ message[:attachments] = attachments_to_postmark(message[:attachments])
19
+ end
20
+
21
+ HashHelper.to_postmark(message)
22
+ end
23
+
24
+ def headers_to_postmark(headers)
25
+ wrap_in_array(headers).map do |item|
26
+ HashHelper.to_postmark(item)
27
+ end
28
+ end
29
+
30
+ def attachments_to_postmark(attachments)
31
+ wrap_in_array(attachments).map do |item|
32
+ if item.is_a?(Hash)
33
+ HashHelper.to_postmark(item)
34
+ elsif item.is_a?(File)
35
+ {
36
+ "Name" => item.path.split("/")[-1],
37
+ "Content" => encode_in_base64(IO.read(item.path)),
38
+ "ContentType" => "application/octet-stream"
39
+ }
40
+ end
41
+ end
42
+ end
43
+
44
+ def encode_in_base64(data)
45
+ [data].pack('m')
46
+ end
47
+
48
+ protected
49
+
50
+ # From ActiveSupport (Array#wrap)
51
+ def wrap_in_array(object)
52
+ if object.nil?
53
+ []
54
+ elsif object.respond_to?(:to_ary)
55
+ object.to_ary || [object]
56
+ else
57
+ [object]
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -2,11 +2,26 @@ require 'thread' unless defined? Mutex # For Ruby 1.8.7
2
2
  require 'cgi'
3
3
 
4
4
  module Postmark
5
- module HttpClient
6
- extend self
7
-
8
- @@client_mutex = Mutex.new
9
- @@request_mutex = Mutex.new
5
+ class HttpClient
6
+ attr_accessor :api_key
7
+ attr_reader :http, :secure, :proxy_host, :proxy_port, :proxy_user,
8
+ :proxy_pass, :host, :port, :path_prefix,
9
+ :http_open_timeout, :http_read_timeout
10
+
11
+ DEFAULTS = {
12
+ :host => 'api.postmarkapp.com',
13
+ :secure => false,
14
+ :path_prefix => '/',
15
+ :http_read_timeout => 15,
16
+ :http_open_timeout => 5
17
+ }
18
+
19
+ def initialize(api_key, options = {})
20
+ @api_key = api_key
21
+ @request_mutex = Mutex.new
22
+ apply_options(options)
23
+ @http = build_http
24
+ end
10
25
 
11
26
  def post(path, data = '')
12
27
  do_request { |client| client.post(url_path(path), data, headers) }
@@ -22,17 +37,25 @@ module Postmark
22
37
 
23
38
  protected
24
39
 
40
+ def apply_options(options = {})
41
+ options = Hash[*options.select { |_, v| !v.nil? }.flatten]
42
+ DEFAULTS.merge(options).each_pair do |name, value|
43
+ instance_variable_set(:"@#{name}", value)
44
+ end
45
+ @port = options[:port] || @secure ? 443 : 80
46
+ end
47
+
25
48
  def to_query_string(hash)
26
49
  return "" if hash.empty?
27
50
  "?" + hash.map { |key, value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" }.join("&")
28
51
  end
29
52
 
30
53
  def protocol
31
- Postmark.secure ? "https" : "http"
54
+ self.secure ? "https" : "http"
32
55
  end
33
56
 
34
57
  def url
35
- URI.parse("#{protocol}://#{Postmark.host}:#{Postmark.port}/")
58
+ URI.parse("#{protocol}://#{self.host}:#{self.port}/")
36
59
  end
37
60
 
38
61
  def handle_response(response)
@@ -51,37 +74,30 @@ module Postmark
51
74
  end
52
75
 
53
76
  def headers
54
- @headers ||= HEADERS.merge({ "X-Postmark-Server-Token" => Postmark.api_key.to_s })
77
+ HEADERS.merge({ "X-Postmark-Server-Token" => self.api_key.to_s })
55
78
  end
56
79
 
57
80
  def url_path(path)
58
- Postmark.path_prefix + path
81
+ self.path_prefix + path
59
82
  end
60
83
 
61
84
  def do_request
62
- @@request_mutex.synchronize do
85
+ @request_mutex.synchronize do
63
86
  handle_response(yield(http))
64
87
  end
65
- end
66
-
67
- def http
68
- return @http if @http
69
-
70
- @@client_mutex.synchronize do
71
- return @http if @http
72
- @http = build_http
73
- end
88
+ rescue Timeout::Error
89
+ raise TimeoutError.new($!)
74
90
  end
75
91
 
76
92
  def build_http
77
- http = Net::HTTP::Proxy(Postmark.proxy_host,
78
- Postmark.proxy_port,
79
- Postmark.proxy_user,
80
- Postmark.proxy_pass).new(url.host, url.port)
81
-
82
- http.read_timeout = Postmark.http_read_timeout
83
- http.open_timeout = Postmark.http_open_timeout
84
- http.use_ssl = !!Postmark.secure
93
+ http = Net::HTTP::Proxy(self.proxy_host,
94
+ self.proxy_port,
95
+ self.proxy_user,
96
+ self.proxy_pass).new(url.host, url.port)
97
+
98
+ http.read_timeout = self.http_read_timeout
99
+ http.open_timeout = self.http_open_timeout
100
+ http.use_ssl = !!self.secure
85
101
  http
86
102
  end
87
103
 
@@ -91,7 +107,7 @@ module Postmark
91
107
 
92
108
  def error_message_and_code(response_body)
93
109
  reply = Postmark::Json.decode(response_body)
94
- [reply["Message"], reply["ErrorCode"]]
110
+ [reply["Message"], reply["ErrorCode"], reply]
95
111
  end
96
112
 
97
113
  def error(clazz, response_body)
@@ -0,0 +1,21 @@
1
+ module Postmark
2
+ module Inbound
3
+ extend self
4
+
5
+ def to_ruby_hash(inbound)
6
+ inbound = Json.decode(inbound) if inbound.is_a?(String)
7
+ ret = HashHelper.to_ruby(inbound)
8
+ ret[:from_full] ||= {}
9
+ ret[:to_full] ||= []
10
+ ret[:cc_full] ||= []
11
+ ret[:headers] ||= []
12
+ ret[:attachments] ||= []
13
+ ret[:from_full] = HashHelper.to_ruby(ret[:from_full])
14
+ ret[:to_full] = ret[:to_full].map { |to| HashHelper.to_ruby(to) }
15
+ ret[:cc_full] = ret[:cc_full].map { |cc| HashHelper.to_ruby(cc) }
16
+ ret[:headers] = ret[:headers].map { |h| HashHelper.to_ruby(h) }
17
+ ret[:attachments] = ret[:attachments].map { |a| HashHelper.to_ruby(a) }
18
+ ret
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ module Postmark
2
+ module Inflector
3
+
4
+ extend self
5
+
6
+ def to_postmark(name)
7
+ name.to_s.split('_').map { |part| capitalize_first_letter(part) }.join('')
8
+ end
9
+
10
+ def to_ruby(name)
11
+ name.to_s.scan(camel_case_regexp).join('_').downcase.to_sym
12
+ end
13
+
14
+ def camel_case_regexp
15
+ /(?:[A-Z](?:(?:[A-Z]+(?![a-z\d]))|[a-z\d]*))|[a-z\d\_]+/
16
+ end
17
+
18
+ protected
19
+
20
+ def capitalize_first_letter(str)
21
+ if str.length > 0
22
+ str.slice(0..0).capitalize + str.slice(1..-1)
23
+ else
24
+ str
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,12 +1,12 @@
1
1
  module Mail
2
2
  class Message
3
-
3
+
4
4
  include Postmark::SharedMessageExtensions
5
-
5
+
6
6
  def html?
7
7
  content_type && content_type.include?('text/html')
8
8
  end
9
-
9
+
10
10
  def body_html
11
11
  if html_part.nil?
12
12
  body.to_s if html?
@@ -17,7 +17,7 @@ module Mail
17
17
 
18
18
  def body_text
19
19
  if text_part.nil?
20
- body.to_s
20
+ body.to_s unless html?
21
21
  else
22
22
  text_part.body.to_s
23
23
  end
@@ -27,6 +27,39 @@ module Mail
27
27
  export_native_attachments + postmark_attachments
28
28
  end
29
29
 
30
+ def export_headers
31
+ [].tap do |headers|
32
+ self.header.fields.each do |field|
33
+ key, value = field.name, field.value
34
+ next if bogus_headers.include? key.downcase
35
+ name = key.split(/-/).map { |i| i.capitalize }.join('-')
36
+
37
+ headers << { "Name" => name, "Value" => value }
38
+ end
39
+ end
40
+ end
41
+
42
+ def to_postmark_hash
43
+ options = Hash.new
44
+ headers = self.export_headers
45
+ attachments = self.export_attachments
46
+
47
+ options["From"] = self['from'].to_s if self.from
48
+ options["Subject"] = self.subject
49
+ options["Attachments"] = attachments unless attachments.empty?
50
+ options["Headers"] = headers if headers.size > 0
51
+ options["HtmlBody"] = self.body_html
52
+ options["TextBody"] = self.body_text
53
+ options["Tag"] = self.tag.to_s if self.tag
54
+
55
+ %w(to reply_to cc bcc).each do |field|
56
+ next unless value = self.send(field)
57
+ options[::Postmark::Inflector.to_postmark(field)] = Array[value].flatten.join(", ")
58
+ end
59
+
60
+ options.delete_if { |k,v| v.nil? || v.empty? }
61
+ end
62
+
30
63
  protected
31
64
 
32
65
  def export_native_attachments
@@ -36,6 +69,18 @@ module Mail
36
69
  "ContentType" => attachment.mime_type}
37
70
  end
38
71
  end
39
-
72
+
73
+ def bogus_headers
74
+ %q[
75
+ return-path x-pm-rcpt
76
+ from reply-to
77
+ sender received
78
+ date content-type
79
+ cc bcc
80
+ subject tag
81
+ attachment
82
+ ]
83
+ end
84
+
40
85
  end
41
86
  end
@@ -1,49 +1,44 @@
1
1
  module Postmark
2
2
  module SharedMessageExtensions
3
3
 
4
- def tag
5
- self['TAG']
4
+ def self.included(klass)
5
+ klass.instance_eval do
6
+ attr_accessor :delivered, :postmark_response
7
+ end
8
+ end
9
+
10
+ def delivered?
11
+ self.delivered
12
+ end
13
+
14
+ def tag(val = nil)
15
+ default 'TAG', val
6
16
  end
7
17
 
8
- def tag=(value)
9
- self['TAG'] = value
18
+ def tag=(val)
19
+ header['TAG'] = val
10
20
  end
11
21
 
12
22
  def postmark_attachments=(value)
13
- @_attachments = wrap_in_array(value)
23
+ Kernel.warn("Mail::Message#postmark_attachments= is deprecated and will " \
24
+ "be removed in the future. Please consider using the native " \
25
+ "attachments API provided by Mail library.")
26
+ @_attachments = value
14
27
  end
15
28
 
16
29
  def postmark_attachments
17
30
  return [] if @_attachments.nil?
31
+ Kernel.warn("Mail::Message#postmark_attachments is deprecated and will " \
32
+ "be removed in the future. Please consider using the native " \
33
+ "attachments API provided by Mail library.")
18
34
 
19
- @_attachments.map do |item|
20
- if item.is_a?(Hash)
21
- item
22
- elsif item.is_a?(File)
23
- {
24
- "Name" => item.path.split("/")[-1],
25
- "Content" => pack_attachment_data(IO.read(item.path)),
26
- "ContentType" => "application/octet-stream"
27
- }
28
- end
29
- end
35
+ Postmark::MessageHelper.attachments_to_postmark(@_attachments)
30
36
  end
31
37
 
32
38
  protected
33
39
 
34
40
  def pack_attachment_data(data)
35
- [data].pack('m')
36
- end
37
-
38
- # From ActiveSupport (Array#wrap)
39
- def wrap_in_array(object)
40
- if object.nil?
41
- []
42
- elsif object.respond_to?(:to_ary)
43
- object.to_ary || [object]
44
- else
45
- [object]
46
- end
41
+ MessageHelper.encode_in_base64(data)
47
42
  end
48
43
 
49
44
  end
@@ -1,3 +1,3 @@
1
1
  module Postmark
2
- VERSION = "0.9.19"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -23,7 +23,9 @@ Gem::Specification.new do |s|
23
23
 
24
24
  s.post_install_message = %q{
25
25
  ==================
26
- Thanks for installing the postmark gem. If you don't have an account, please sign up at http://postmarkapp.com/.
26
+ Thanks for installing the postmark gem. If you don't have an account, please
27
+ sign up at http://postmarkapp.com/.
28
+
27
29
  Review the README.md for implementation details and examples.
28
30
  ==================
29
31
  }
@@ -33,12 +35,7 @@ Gem::Specification.new do |s|
33
35
  s.add_dependency "rake"
34
36
  s.add_dependency "json"
35
37
 
36
- s.add_development_dependency "tmail"
37
38
  s.add_development_dependency "mail"
38
- s.add_development_dependency "rspec-core", "~> 2.0"
39
39
  s.add_development_dependency "activesupport", "~> 3.0"
40
- s.add_development_dependency "fakeweb"
41
- s.add_development_dependency "fakeweb-matcher"
42
- s.add_development_dependency "timecop"
43
- s.add_development_dependency "yajl-ruby"
40
+ s.add_development_dependency "yajl-ruby" unless RUBY_PLATFORM == "java"
44
41
  end