dotmailer 0.0.3 → 0.0.4
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.
- checksums.yaml +6 -14
- data/.gitignore +0 -1
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile.lock +58 -0
- data/README.md +75 -2
- data/Rakefile +9 -0
- data/dotmailer.gemspec +1 -0
- data/lib/dot_mailer.rb +6 -1
- data/lib/dot_mailer/account.rb +22 -2
- data/lib/dot_mailer/campaign.rb +95 -0
- data/lib/dot_mailer/campaign_summary.rb +77 -0
- data/lib/dot_mailer/client.rb +7 -0
- data/lib/dot_mailer/contact.rb +4 -0
- data/lib/dot_mailer/contact_import.rb +21 -3
- data/lib/dot_mailer/exceptions.rb +6 -0
- data/lib/dot_mailer/from_address.rb +36 -0
- data/lib/dot_mailer/segment.rb +36 -0
- data/lib/dot_mailer/version.rb +1 -1
- data/spec/dot_mailer/account_spec.rb +57 -8
- data/spec/dot_mailer/campaign_spec.rb +206 -0
- data/spec/dot_mailer/campaign_summary_spec.rb +20 -0
- data/spec/dot_mailer/client_spec.rb +15 -1
- data/spec/dot_mailer/contact_import_spec.rb +47 -6
- data/spec/dot_mailer/contact_spec.rb +9 -1
- data/spec/dot_mailer/from_address_spec.rb +25 -0
- data/spec/dot_mailer/segment_spec.rb +80 -0
- data/spec/dot_mailer/suppression_spec.rb +2 -2
- data/spec/spec_helper.rb +2 -0
- metadata +38 -13
data/lib/dot_mailer/client.rb
CHANGED
data/lib/dot_mailer/contact.rb
CHANGED
@@ -2,11 +2,15 @@ require 'csv'
|
|
2
2
|
require 'active_support/core_ext/object/blank'
|
3
3
|
|
4
4
|
module DotMailer
|
5
|
+
# This is the maximum number of times we will poll the dotMailer
|
6
|
+
# API to see if an import has finished.
|
7
|
+
MAX_TRIES = 10
|
8
|
+
|
5
9
|
class ContactImport
|
6
|
-
def self.import(account, contacts)
|
10
|
+
def self.import(account, contacts, wait_for_finish = false)
|
7
11
|
contact_import = new(account, contacts)
|
8
12
|
|
9
|
-
contact_import.start
|
13
|
+
contact_import.start(wait_for_finish)
|
10
14
|
|
11
15
|
contact_import
|
12
16
|
end
|
@@ -18,12 +22,14 @@ module DotMailer
|
|
18
22
|
self.contacts = contacts
|
19
23
|
end
|
20
24
|
|
21
|
-
def start
|
25
|
+
def start(wait_for_finish)
|
22
26
|
validate_headers
|
23
27
|
|
24
28
|
response = client.post_csv '/contacts/import', contacts_csv
|
25
29
|
|
26
30
|
self.id = response['id']
|
31
|
+
|
32
|
+
wait_until_finished if wait_for_finish
|
27
33
|
end
|
28
34
|
|
29
35
|
def status
|
@@ -90,5 +96,17 @@ module DotMailer
|
|
90
96
|
def valid_headers
|
91
97
|
@valid_headers ||= %w(id email optInType emailType) + account.data_fields.map(&:name)
|
92
98
|
end
|
99
|
+
|
100
|
+
def wait_until_finished
|
101
|
+
# Wait for the import to finish, backing off in incremental powers
|
102
|
+
# of 2, a maximum of MAX_TRIES times.
|
103
|
+
#
|
104
|
+
# (i.e. 1s, 4s, 9s, 16s, ..., MAX_TRIES ** 2)
|
105
|
+
#
|
106
|
+
# A MAX_TRIES of 10 means we will wait a total of 385 seconds before
|
107
|
+
# giving up.
|
108
|
+
finished = (1..MAX_TRIES).detect { |i| sleep(i ** 2) && finished? }
|
109
|
+
raise ImportNotFinished unless finished
|
110
|
+
end
|
93
111
|
end
|
94
112
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_support/core_ext/hash/slice'
|
2
|
+
|
3
|
+
module DotMailer
|
4
|
+
class FromAddress
|
5
|
+
def initialize(attributes)
|
6
|
+
self.attributes = attributes
|
7
|
+
end
|
8
|
+
|
9
|
+
def id
|
10
|
+
attributes['id']
|
11
|
+
end
|
12
|
+
|
13
|
+
def email
|
14
|
+
attributes['email']
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
attributes.slice('id', 'email')
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other)
|
22
|
+
attributes == other.attributes
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
%{#{self.class.name} id: #{id}, email: #{email}}
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect
|
30
|
+
to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
attr_accessor :attributes
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module DotMailer
|
2
|
+
class Segment
|
3
|
+
def initialize(account, attributes)
|
4
|
+
self.account = account
|
5
|
+
self.attributes = attributes
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.find_by_id(account, id)
|
9
|
+
response = account.client.get "/segments"
|
10
|
+
response = response.detect {|segment| segment['id'] == id}
|
11
|
+
|
12
|
+
new(account, response)
|
13
|
+
end
|
14
|
+
|
15
|
+
def id
|
16
|
+
attributes['id']
|
17
|
+
end
|
18
|
+
|
19
|
+
def refresh!
|
20
|
+
client.post_json "/segments/refresh/#{self.id}", {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def refresh_progress
|
24
|
+
response = client.get "/segments/refresh/#{self.id}"
|
25
|
+
return response["status"]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
attr_accessor :attributes, :account
|
30
|
+
|
31
|
+
def client
|
32
|
+
account.client
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
data/lib/dot_mailer/version.rb
CHANGED
@@ -4,11 +4,15 @@ describe DotMailer::Account do
|
|
4
4
|
let(:api_user) { double 'api user' }
|
5
5
|
let(:api_pass) { double 'api pass' }
|
6
6
|
let(:client) { double 'client' }
|
7
|
+
let(:cache) { double 'cache' }
|
7
8
|
|
8
9
|
subject { DotMailer::Account.new(api_user, api_pass) }
|
9
10
|
|
10
11
|
before(:each) do
|
11
|
-
subject.stub
|
12
|
+
subject.stub(
|
13
|
+
:client => client,
|
14
|
+
:cache => cache
|
15
|
+
)
|
12
16
|
end
|
13
17
|
|
14
18
|
describe '#initialize' do
|
@@ -51,11 +55,6 @@ describe DotMailer::Account do
|
|
51
55
|
|
52
56
|
describe '#data_fields' do
|
53
57
|
let(:data_fields) { double 'data fields' }
|
54
|
-
let(:cache) { double 'cache' }
|
55
|
-
|
56
|
-
before(:each) do
|
57
|
-
subject.stub :cache => cache
|
58
|
-
end
|
59
58
|
|
60
59
|
context 'when the cache is empty' do
|
61
60
|
before(:each) do
|
@@ -90,10 +89,8 @@ describe DotMailer::Account do
|
|
90
89
|
describe '#create_data_field' do
|
91
90
|
let(:name) { double 'name' }
|
92
91
|
let(:options) { double 'options' }
|
93
|
-
let(:cache) { double 'cache' }
|
94
92
|
|
95
93
|
before(:each) do
|
96
|
-
subject.stub :cache => cache
|
97
94
|
DotMailer::DataField.stub :create
|
98
95
|
cache.stub :delete
|
99
96
|
end
|
@@ -110,4 +107,56 @@ describe DotMailer::Account do
|
|
110
107
|
subject.create_data_field(name, options)
|
111
108
|
end
|
112
109
|
end
|
110
|
+
|
111
|
+
describe '#from_addresses' do
|
112
|
+
let(:attributes) { double 'attributes' }
|
113
|
+
let(:response) { 3.times.map { attributes } }
|
114
|
+
let(:from_address) { double 'from address' }
|
115
|
+
let(:from_addresses) { 3.times.map { from_address } }
|
116
|
+
|
117
|
+
before(:each) do
|
118
|
+
DotMailer::FromAddress.stub :new => from_address
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'when the cache is empty' do
|
122
|
+
before(:each) do
|
123
|
+
cache.stub(:fetch).with('from_addresses').and_yield
|
124
|
+
client.stub :get => response
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should call get on the client with the correct path' do
|
128
|
+
client.should_receive(:get).with('/custom-from-addresses')
|
129
|
+
|
130
|
+
subject.from_addresses
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should initialize 3 FromAddresses' do
|
134
|
+
DotMailer::FromAddress.should_receive(:new).with(attributes).exactly(3).times
|
135
|
+
|
136
|
+
subject.from_addresses
|
137
|
+
end
|
138
|
+
|
139
|
+
its(:from_addresses) { should == from_addresses }
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'when the cache is not empty' do
|
143
|
+
before(:each) do
|
144
|
+
cache.stub(:fetch).with('from_addresses').and_return(response)
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should not call get on the client' do
|
148
|
+
client.should_not_receive(:get)
|
149
|
+
|
150
|
+
subject.from_addresses
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should initialize 3 FromAddresses' do
|
154
|
+
DotMailer::FromAddress.should_receive(:new).with(attributes).exactly(3).times
|
155
|
+
|
156
|
+
subject.from_addresses
|
157
|
+
end
|
158
|
+
|
159
|
+
its(:from_addresses) { should == from_addresses }
|
160
|
+
end
|
161
|
+
end
|
113
162
|
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DotMailer::Campaign do
|
4
|
+
let(:client) { double 'client' }
|
5
|
+
let(:account) { double 'account', :client => client }
|
6
|
+
|
7
|
+
let(:id) { 123 }
|
8
|
+
let(:name) { 'my_campaign' }
|
9
|
+
let(:campaign_subject) { 'My Campaign' }
|
10
|
+
let(:from_name) { 'Me' }
|
11
|
+
let(:from_email) { 'me@example.com' }
|
12
|
+
let(:html_content) { '<h1>Hello!</h1><a href="http://$UNSUB$">Unsubscribe</a>' }
|
13
|
+
let(:plain_text_content) { "Hello!\n======\nhttp://$UNSUB$" }
|
14
|
+
|
15
|
+
let(:from_address) do
|
16
|
+
DotMailer::FromAddress.new 'id' => 123, 'email' => from_email
|
17
|
+
end
|
18
|
+
|
19
|
+
subject do
|
20
|
+
DotMailer::Campaign.new(account, {
|
21
|
+
'id' => id,
|
22
|
+
'name' => name,
|
23
|
+
'subject' => campaign_subject,
|
24
|
+
'fromName' => from_name,
|
25
|
+
'fromAddress' => from_address.to_hash,
|
26
|
+
'htmlContent' => html_content,
|
27
|
+
'plainTextContent' => plain_text_content
|
28
|
+
})
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'Class' do
|
32
|
+
subject { DotMailer::Campaign }
|
33
|
+
|
34
|
+
describe '.create' do
|
35
|
+
let(:response) { double 'response' }
|
36
|
+
let(:campaign) { double 'campaign' }
|
37
|
+
|
38
|
+
# We define a method so we can override keys within
|
39
|
+
# context blocks without redefining other keys
|
40
|
+
def attributes
|
41
|
+
{
|
42
|
+
:name => name,
|
43
|
+
:subject => campaign_subject,
|
44
|
+
:from_name => from_name,
|
45
|
+
:from_email => from_email,
|
46
|
+
:html_content => html_content,
|
47
|
+
:plain_text_content => plain_text_content
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
before(:each) do
|
52
|
+
account.stub :from_addresses => [from_address]
|
53
|
+
client.stub :post_json => response
|
54
|
+
subject.stub :new => campaign
|
55
|
+
end
|
56
|
+
|
57
|
+
[
|
58
|
+
:name,
|
59
|
+
:subject,
|
60
|
+
:from_name,
|
61
|
+
:from_email,
|
62
|
+
:html_content,
|
63
|
+
:plain_text_content
|
64
|
+
].each do |attribute|
|
65
|
+
context "without specifying #{attribute}" do
|
66
|
+
define_method :attributes do
|
67
|
+
super().except(attribute)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should raise an error' do
|
71
|
+
expect { subject.create(account, attributes) }.to \
|
72
|
+
raise_error(RuntimeError, "missing :#{attribute}")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'when the fromAddress is not a valid from address' do
|
78
|
+
let(:unknown_email) { 'unknown@example.com' }
|
79
|
+
|
80
|
+
def attributes
|
81
|
+
super.merge(:from_email => unknown_email)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should raise an error' do
|
85
|
+
expect { subject.create(account, attributes) }.to \
|
86
|
+
raise_error(DotMailer::InvalidFromAddress, unknown_email)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should call post_json on the client with the correct path' do
|
91
|
+
client.should_receive(:post_json).with('/campaigns', anything)
|
92
|
+
|
93
|
+
subject.create(account, attributes)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'should call post_json on the client with the correct parameters' do
|
97
|
+
client.should_receive(:post_json).with(anything, {
|
98
|
+
'Name' => name,
|
99
|
+
'Subject' => campaign_subject,
|
100
|
+
'FromName' => from_name,
|
101
|
+
'FromAddress' => from_address.to_hash,
|
102
|
+
'HtmlContent' => html_content,
|
103
|
+
'PlainTextContent' => plain_text_content
|
104
|
+
})
|
105
|
+
|
106
|
+
subject.create(account, attributes)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should instantiate a new Campaign object with the account and response' do
|
110
|
+
subject.should_receive(:new).with(account, response)
|
111
|
+
|
112
|
+
subject.create(account, attributes)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should return the new Campaign object' do
|
116
|
+
subject.create(account, attributes).should == campaign
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe '.find_by_id' do
|
121
|
+
let(:id) { 123 }
|
122
|
+
let(:response) { double 'response' }
|
123
|
+
let(:campaign) { double 'campaign' }
|
124
|
+
|
125
|
+
before(:each) do
|
126
|
+
subject.stub :new => campaign
|
127
|
+
client.stub :get => response
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should call get on the client with the correct parameters' do
|
131
|
+
client.should_receive(:get).with("/campaigns/#{id}")
|
132
|
+
|
133
|
+
subject.find_by_id(account, id)
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should initialize a Campaign with the response' do
|
137
|
+
subject.should_receive(:new).with(account, response)
|
138
|
+
|
139
|
+
subject.find_by_id(account, id)
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'should return the new Campaign object' do
|
143
|
+
subject.find_by_id(account, id).should == campaign
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
its(:id) { should == id }
|
149
|
+
its(:name) { should == name }
|
150
|
+
its(:from_name) { should == from_name }
|
151
|
+
its(:from_address) { should == from_address }
|
152
|
+
its(:html_content) { should == html_content }
|
153
|
+
its(:plain_text_content) { should == plain_text_content }
|
154
|
+
|
155
|
+
describe '#send_to_contact_ids' do
|
156
|
+
let(:contact_ids) { double 'contact ids' }
|
157
|
+
|
158
|
+
it 'should call post_json on the client with the correct path' do
|
159
|
+
client.should_receive(:post_json).with('/campaigns/send', anything)
|
160
|
+
|
161
|
+
subject.send_to_contact_ids contact_ids
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'should call post_json on the client with the contact ids' do
|
165
|
+
client.should_receive(:post_json).with(anything, {
|
166
|
+
'campaignId' => id,
|
167
|
+
'contactIds' => contact_ids
|
168
|
+
})
|
169
|
+
|
170
|
+
subject.send_to_contact_ids contact_ids
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe '#send_to_segment' do
|
175
|
+
let(:segment) { double 'segment', :id => 123 }
|
176
|
+
|
177
|
+
it 'should call post_json on the client with the correct path' do
|
178
|
+
client.should_receive(:post_json).with('/campaigns/send', anything)
|
179
|
+
|
180
|
+
subject.send_to_segment segment
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'should call post_json on the client with the contact ids' do
|
184
|
+
client.should_receive(:post_json).with(anything, {
|
185
|
+
'campaignId' => id,
|
186
|
+
'addressBookIds' => [segment.id]
|
187
|
+
})
|
188
|
+
|
189
|
+
subject.send_to_segment segment
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe '#summary' do
|
194
|
+
let(:summary) { double 'campaign_summary'}
|
195
|
+
|
196
|
+
it 'should call get on the client with the correct path' do
|
197
|
+
client.should_receive(:get).with("/campaigns/#{id}/summary")
|
198
|
+
subject.summary
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'should return a CampaignSummary object' do
|
202
|
+
client.should_receive(:get).with(anything).and_return(summary)
|
203
|
+
subject.summary.class.should be(DotMailer::CampaignSummary)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|