dotmailer 0.0.1 → 0.0.2
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/README.md +162 -11
- data/TODO.md +6 -0
- data/dotmailer.gemspec +4 -3
- data/lib/dot_mailer.rb +11 -0
- data/lib/dot_mailer/account.rb +66 -0
- data/lib/dot_mailer/client.rb +108 -0
- data/lib/dot_mailer/contact.rb +179 -0
- data/lib/dot_mailer/contact_import.rb +94 -0
- data/lib/{dotmailer → dot_mailer}/data_field.rb +24 -1
- data/lib/dot_mailer/exceptions.rb +16 -0
- data/lib/dot_mailer/opt_in_type.rb +16 -0
- data/lib/dot_mailer/suppression.rb +29 -0
- data/lib/dot_mailer/version.rb +3 -0
- data/lib/dotmailer.rb +1 -3
- data/spec/dot_mailer/account_spec.rb +113 -0
- data/spec/dot_mailer/client_spec.rb +160 -0
- data/spec/dot_mailer/contact_import_spec.rb +173 -0
- data/spec/dot_mailer/contact_spec.rb +303 -0
- data/spec/dot_mailer/data_field_spec.rb +109 -0
- data/spec/dot_mailer/opt_in_type_spec.rb +21 -0
- data/spec/dot_mailer/suppression_spec.rb +67 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/assignable_attributes_helper.rb +20 -0
- metadata +37 -8
- data/lib/dotmailer/client.rb +0 -78
- data/lib/dotmailer/exceptions.rb +0 -4
- data/lib/dotmailer/version.rb +0 -3
- data/spec/dotmailer/client_spec.rb +0 -159
- data/spec/dotmailer/data_field_spec.rb +0 -32
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
|
4
|
+
module DotMailer
|
5
|
+
class ContactImport
|
6
|
+
def self.import(account, contacts)
|
7
|
+
contact_import = new(account, contacts)
|
8
|
+
|
9
|
+
contact_import.start
|
10
|
+
|
11
|
+
contact_import
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :id
|
15
|
+
|
16
|
+
def initialize(account, contacts)
|
17
|
+
self.account = account
|
18
|
+
self.contacts = contacts
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
validate_headers
|
23
|
+
|
24
|
+
response = client.post_csv '/contacts/import', contacts_csv
|
25
|
+
|
26
|
+
self.id = response['id']
|
27
|
+
end
|
28
|
+
|
29
|
+
def status
|
30
|
+
if id.nil?
|
31
|
+
'NotStarted'
|
32
|
+
else
|
33
|
+
response = client.get "/contacts/import/#{id}"
|
34
|
+
|
35
|
+
response['status']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def finished?
|
40
|
+
status == 'Finished'
|
41
|
+
end
|
42
|
+
|
43
|
+
def errors
|
44
|
+
raise ImportNotFinished unless finished?
|
45
|
+
|
46
|
+
client.get_csv "/contacts/import/#{id}/report-faults"
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
"#{self.class.name} contacts: #{contacts.to_s}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
attr_accessor :contacts, :account
|
59
|
+
attr_writer :id
|
60
|
+
|
61
|
+
def client
|
62
|
+
account.client
|
63
|
+
end
|
64
|
+
|
65
|
+
def contact_headers
|
66
|
+
@contact_headers ||= contacts.map(&:keys).flatten.uniq
|
67
|
+
end
|
68
|
+
|
69
|
+
def contacts_csv
|
70
|
+
@contacts_csv ||= CSV.generate do |csv|
|
71
|
+
csv << contact_headers
|
72
|
+
|
73
|
+
contacts.each do |contact|
|
74
|
+
csv << contact_headers.map { |header| contact[header] }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Check that the contact_headers are all valid (case insensitive)
|
80
|
+
def validate_headers
|
81
|
+
raise UnknownDataField, unknown_headers.join(',') if unknown_headers.present?
|
82
|
+
end
|
83
|
+
|
84
|
+
def unknown_headers
|
85
|
+
@unknown_headers ||= contact_headers.reject do |header|
|
86
|
+
valid_headers.map(&:downcase).include?(header.downcase)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def valid_headers
|
91
|
+
@valid_headers ||= %w(id email optInType emailType) + account.data_fields.map(&:name)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,5 +1,24 @@
|
|
1
|
-
module
|
1
|
+
module DotMailer
|
2
2
|
class DataField
|
3
|
+
def self.all(account)
|
4
|
+
fields = account.client.get '/data-fields'
|
5
|
+
|
6
|
+
fields.map { |attributes| new(attributes) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.create(account, name, options = {})
|
10
|
+
options[:type] ||= 'String'
|
11
|
+
options[:visibility] ||= 'Public'
|
12
|
+
|
13
|
+
account.client.post_json(
|
14
|
+
'/data-fields',
|
15
|
+
'name' => name,
|
16
|
+
'type' => options[:type],
|
17
|
+
'visibility' => options[:visibility],
|
18
|
+
'defaultValue' => options[:default]
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
3
22
|
def initialize(attributes)
|
4
23
|
self.attributes = attributes
|
5
24
|
end
|
@@ -32,6 +51,10 @@ module Dotmailer
|
|
32
51
|
attributes == other.attributes
|
33
52
|
end
|
34
53
|
|
54
|
+
def date?
|
55
|
+
type == 'Date'
|
56
|
+
end
|
57
|
+
|
35
58
|
protected
|
36
59
|
attr_accessor :attributes
|
37
60
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module DotMailer
|
2
|
+
module OptInType
|
3
|
+
DOUBLE = 'Double'
|
4
|
+
SINGLE = 'Single'
|
5
|
+
UNKNOWN = 'Unknown'
|
6
|
+
VERIFIED_DOUBLE = 'VerifiedDouble'
|
7
|
+
|
8
|
+
def self.all
|
9
|
+
constants(false).map(&method(:const_get))
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.exists?(opt_in_type)
|
13
|
+
all.include?(opt_in_type)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module DotMailer
|
4
|
+
class Suppression
|
5
|
+
attr_reader :contact, :date_removed, :reason
|
6
|
+
|
7
|
+
def self.suppressed_since(account, time)
|
8
|
+
response = account.client.get("/contacts/suppressed-since/#{time.utc.xmlschema}")
|
9
|
+
|
10
|
+
response.map do |attributes|
|
11
|
+
new(account, attributes)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(account, attributes)
|
16
|
+
@contact = Contact.new account, attributes['suppressedContact']
|
17
|
+
@date_removed = Time.parse attributes['dateRemoved']
|
18
|
+
@reason = attributes['reason']
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
%{#{self.class.name} reason: #{reason}, date_removed: #{date_removed}, contact: #{contact.to_s}}
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
to_s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/dotmailer.rb
CHANGED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DotMailer::Account do
|
4
|
+
let(:api_user) { double 'api user' }
|
5
|
+
let(:api_pass) { double 'api pass' }
|
6
|
+
let(:client) { double 'client' }
|
7
|
+
|
8
|
+
subject { DotMailer::Account.new(api_user, api_pass) }
|
9
|
+
|
10
|
+
before(:each) do
|
11
|
+
subject.stub :client => client
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#initialize' do
|
15
|
+
before(:each) do
|
16
|
+
DotMailer::Client.stub :new => client
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should initialize a Client with the credentials' do
|
20
|
+
DotMailer::Client.should_receive(:new).with(api_user, api_pass)
|
21
|
+
|
22
|
+
DotMailer::Account.new(api_user, api_pass)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should set the client' do
|
26
|
+
DotMailer::Client.should_receive(:new).with(api_user, api_pass)
|
27
|
+
|
28
|
+
account = DotMailer::Account.new(api_user, api_pass)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#suppress' do
|
33
|
+
let(:email) { double 'email' }
|
34
|
+
|
35
|
+
before(:each) do
|
36
|
+
client.stub :post_json
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should call post_json on the client with the correct path' do
|
40
|
+
client.should_receive(:post_json).with('/contacts/unsubscribe', anything)
|
41
|
+
|
42
|
+
subject.suppress email
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should call post_json on the client with the email address' do
|
46
|
+
client.should_receive(:post_json).with(anything, 'Email' => email)
|
47
|
+
|
48
|
+
subject.suppress email
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#data_fields' do
|
53
|
+
let(:data_fields) { double 'data fields' }
|
54
|
+
let(:cache) { double 'cache' }
|
55
|
+
|
56
|
+
before(:each) do
|
57
|
+
subject.stub :cache => cache
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'when the cache is empty' do
|
61
|
+
before(:each) do
|
62
|
+
cache.stub(:fetch).with('data_fields').and_yield
|
63
|
+
DotMailer::DataField.stub :all => data_fields
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should call DataField.all' do
|
67
|
+
DotMailer::DataField.should_receive(:all).with(subject)
|
68
|
+
|
69
|
+
subject.data_fields
|
70
|
+
end
|
71
|
+
|
72
|
+
its(:data_fields) { should == data_fields }
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'when the cache is not empty' do
|
76
|
+
before(:each) do
|
77
|
+
cache.stub(:fetch).with('data_fields').and_return(data_fields)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should not call DataField.all' do
|
81
|
+
DotMailer::DataField.should_not_receive(:all)
|
82
|
+
|
83
|
+
subject.data_fields
|
84
|
+
end
|
85
|
+
|
86
|
+
its(:data_fields) { should == data_fields }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#create_data_field' do
|
91
|
+
let(:name) { double 'name' }
|
92
|
+
let(:options) { double 'options' }
|
93
|
+
let(:cache) { double 'cache' }
|
94
|
+
|
95
|
+
before(:each) do
|
96
|
+
subject.stub :cache => cache
|
97
|
+
DotMailer::DataField.stub :create
|
98
|
+
cache.stub :delete
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should DataField.create' do
|
102
|
+
DotMailer::DataField.should_receive(:create).with(subject, name, options)
|
103
|
+
|
104
|
+
subject.create_data_field(name, options)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should clear the data_fields from the cache' do
|
108
|
+
cache.should_receive(:delete).with('data_fields')
|
109
|
+
|
110
|
+
subject.create_data_field(name, options)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DotMailer::Client do
|
4
|
+
let(:api_user) { 'john_doe' }
|
5
|
+
let(:api_pass) { 's3cr3t' }
|
6
|
+
let(:api_base_url) { "https://#{api_user}:#{api_pass}@api.dotmailer.com" }
|
7
|
+
let(:api_path) { '/some/api/path' }
|
8
|
+
let(:api_endpoint) { "#{api_base_url}/v2#{api_path}" }
|
9
|
+
|
10
|
+
subject { DotMailer::Client.new(api_user, api_pass) }
|
11
|
+
|
12
|
+
describe '#get' do
|
13
|
+
let(:response) { { 'foo' => 'bar' } }
|
14
|
+
|
15
|
+
before(:each) do
|
16
|
+
stub_request(:get, api_endpoint).to_return(:body => response.to_json)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should GET the endpoint with a JSON accept header' do
|
20
|
+
subject.get api_path
|
21
|
+
|
22
|
+
WebMock.should have_requested(:get, api_endpoint).with(
|
23
|
+
:headers => { 'Accept' => 'application/json' }
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should return the response from the endpoint' do
|
28
|
+
subject.get(api_path).should == response
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when the path is not found' do
|
32
|
+
let(:error_message) { 'not found' }
|
33
|
+
let(:response) { { 'message' => error_message } }
|
34
|
+
|
35
|
+
before(:each) do
|
36
|
+
stub_request(:get, api_endpoint).to_return(:status => 404, :body => response.to_json)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should raise a NotFound error with the error message' do
|
40
|
+
expect { subject.get(api_path).should }.to raise_error(DotMailer::NotFound, error_message)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#get_csv' do
|
46
|
+
# The API includes a UTF-8 BOM in the response...
|
47
|
+
let(:response) { "\xEF\xBB\xBFId,Name\n1,Foo\n2,Bar" }
|
48
|
+
let(:csv) { double 'csv' }
|
49
|
+
|
50
|
+
before(:each) do
|
51
|
+
stub_request(:get, api_endpoint).to_return(:body => response)
|
52
|
+
CSV.stub(:parse => csv)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should GET the endpoint with a CSV accept header' do
|
56
|
+
subject.get_csv api_path
|
57
|
+
|
58
|
+
WebMock.should have_requested(:get, api_endpoint).with(
|
59
|
+
:headers => { 'Accept' => 'text/csv' }
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should pass the response to CSV.parse with the correct options' do
|
64
|
+
CSV.should_receive(:parse).with(response, :headers => true)
|
65
|
+
|
66
|
+
subject.get_csv api_path
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should return the CSV object' do
|
70
|
+
subject.get_csv(api_path).should == csv
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#post' do
|
75
|
+
let(:data) { 'some random data' }
|
76
|
+
let(:response) { { 'foo' => 'bar' } }
|
77
|
+
|
78
|
+
before(:each) do
|
79
|
+
stub_request(:post, api_endpoint).to_return(:body => response.to_json)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should POST the data to the endpoint with a JSON accept header' do
|
83
|
+
subject.post api_path, data
|
84
|
+
|
85
|
+
WebMock.should have_requested(:post, api_endpoint).with(
|
86
|
+
:headers => { 'Accept' => 'application/json' },
|
87
|
+
:body => data
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should return the response from the endpoint' do
|
92
|
+
subject.post(api_path, data).should == response
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'when the data is invalid for the endpoint' do
|
96
|
+
let(:error_message) { 'invalid data' }
|
97
|
+
let(:response) { { 'message' => error_message } }
|
98
|
+
|
99
|
+
before(:each) do
|
100
|
+
stub_request(:post, api_endpoint).to_return(:status => 400, :body => response.to_json)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should raise an InvalidRequest error with the error message' do
|
104
|
+
expect { subject.post(api_path, data) }.to raise_error(DotMailer::InvalidRequest, error_message)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe '#post_json' do
|
110
|
+
let(:params) { { 'foo' => 'bar' } }
|
111
|
+
|
112
|
+
it 'should call post with the path' do
|
113
|
+
subject.should_receive(:post).with(api_path, anything, anything)
|
114
|
+
|
115
|
+
subject.post_json api_path, params
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'should convert the params to JSON' do
|
119
|
+
subject.should_receive(:post).with(anything, params.to_json, anything)
|
120
|
+
|
121
|
+
subject.post_json api_path, params
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should pass use the correct content type' do
|
125
|
+
subject.should_receive(:post).with(anything, anything, hash_including(:content_type => :json))
|
126
|
+
|
127
|
+
subject.post_json api_path, params
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe '#post_csv' do
|
132
|
+
let(:csv) { "Some\nCSV\nString" }
|
133
|
+
let(:tempfile) { double 'tempfile', :write => true, :rewind => true }
|
134
|
+
|
135
|
+
before(:each) do
|
136
|
+
Tempfile.stub :new => tempfile
|
137
|
+
subject.stub :post => double
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'should call post with the path' do
|
141
|
+
subject.should_receive(:post).with(api_path, anything)
|
142
|
+
|
143
|
+
subject.post_csv api_path, csv
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should create a Tempfile with the contents and rewind it' do
|
147
|
+
Tempfile.should_receive(:new).and_return(tempfile)
|
148
|
+
tempfile.should_receive(:write).with(csv)
|
149
|
+
tempfile.should_receive(:rewind)
|
150
|
+
|
151
|
+
subject.post_csv api_path, csv
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'should call post with the tempfile' do
|
155
|
+
subject.should_receive(:post).with(api_path, hash_including(:csv => tempfile))
|
156
|
+
|
157
|
+
subject.post_csv api_path, csv
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|