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.
- data/.travis.yml +8 -0
- data/CHANGELOG.rdoc +20 -0
- data/Gemfile +6 -0
- data/README.md +351 -91
- data/VERSION +1 -1
- data/lib/postmark.rb +40 -132
- data/lib/postmark/api_client.rb +162 -0
- data/lib/postmark/bounce.rb +20 -17
- data/lib/postmark/handlers/mail.rb +10 -3
- data/lib/postmark/helpers/hash_helper.rb +35 -0
- data/lib/postmark/helpers/message_helper.rb +62 -0
- data/lib/postmark/http_client.rb +44 -28
- data/lib/postmark/inbound.rb +21 -0
- data/lib/postmark/inflector.rb +28 -0
- data/lib/postmark/message_extensions/mail.rb +50 -5
- data/lib/postmark/message_extensions/shared.rb +23 -28
- data/lib/postmark/version.rb +1 -1
- data/postmark.gemspec +4 -7
- data/spec/data/empty.gif +0 -0
- data/spec/integration/api_client_hashes_spec.rb +101 -0
- data/spec/integration/api_client_messages_spec.rb +127 -0
- data/spec/integration/mail_delivery_method_spec.rb +80 -0
- data/spec/spec_helper.rb +15 -5
- data/spec/support/helpers.rb +11 -0
- data/spec/{shared_examples.rb → support/shared_examples.rb} +0 -0
- data/spec/unit/postmark/api_client_spec.rb +246 -0
- data/spec/unit/postmark/bounce_spec.rb +142 -0
- data/spec/unit/postmark/handlers/mail_spec.rb +39 -0
- data/spec/unit/postmark/helpers/hash_helper_spec.rb +34 -0
- data/spec/unit/postmark/helpers/message_helper_spec.rb +115 -0
- data/spec/unit/postmark/http_client_spec.rb +204 -0
- data/spec/unit/postmark/inbound_spec.rb +88 -0
- data/spec/unit/postmark/inflector_spec.rb +35 -0
- data/spec/unit/postmark/json_spec.rb +37 -0
- data/spec/unit/postmark/message_extensions/mail_spec.rb +205 -0
- data/spec/unit/postmark_spec.rb +164 -0
- metadata +45 -93
- data/lib/postmark/attachments_fix_for_mail.rb +0 -48
- data/lib/postmark/message_extensions/tmail.rb +0 -115
- data/spec/bounce_spec.rb +0 -53
- 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
|
data/lib/postmark/http_client.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
54
|
+
self.secure ? "https" : "http"
|
32
55
|
end
|
33
56
|
|
34
57
|
def url
|
35
|
-
URI.parse("#{protocol}://#{
|
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
|
-
|
77
|
+
HEADERS.merge({ "X-Postmark-Server-Token" => self.api_key.to_s })
|
55
78
|
end
|
56
79
|
|
57
80
|
def url_path(path)
|
58
|
-
|
81
|
+
self.path_prefix + path
|
59
82
|
end
|
60
83
|
|
61
84
|
def do_request
|
62
|
-
|
85
|
+
@request_mutex.synchronize do
|
63
86
|
handle_response(yield(http))
|
64
87
|
end
|
65
|
-
|
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(
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
http.read_timeout =
|
83
|
-
http.open_timeout =
|
84
|
-
http.use_ssl = !!
|
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
|
5
|
-
|
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=(
|
9
|
-
|
18
|
+
def tag=(val)
|
19
|
+
header['TAG'] = val
|
10
20
|
end
|
11
21
|
|
12
22
|
def postmark_attachments=(value)
|
13
|
-
|
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
|
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
|
-
|
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
|
data/lib/postmark/version.rb
CHANGED
data/postmark.gemspec
CHANGED
@@ -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
|
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 "
|
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
|