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