emarsys-broadcast 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ test.rb
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.2"
4
+ - "1.9.3"
5
+ - "2.0.0"
6
+ - "jruby-19mode"
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Valentin Vasilyev
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # Emarsys::Broadcast
2
+
3
+ Ruby wrapper for Emarsys batch mailing API
4
+ ==========================================
5
+ [![Build Status](https://travis-ci.org/Valve/emarsys-broadcast-ruby.png)](https://travis-ci.org/Valve/emarsys-broadcast-ruby)
6
+ [![Code Climate](https://codeclimate.com/github/Valve/emarsys-broadcast-ruby.png)](https://codeclimate.com/github/Valve/emarsys-broadcast-ruby)
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'emarsys-broadcast'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install emarsys-broadcast
21
+
22
+ ## Usage
23
+
24
+ ### Complete sending example:
25
+
26
+ Minimal configuration is required before usage
27
+
28
+ ```ruby
29
+ # if using rails, place this configuration code into initializer
30
+ Emarsys::Broadcast::configure do |c|
31
+ c.api_user = your_api_user
32
+ c.api_password = your_api_password
33
+
34
+ c.sftp_user = your_sftp_user
35
+ c.sftp_password = your_sftp_password
36
+
37
+ c.sender_domain = 'mail.your.company.com'
38
+ end
39
+
40
+
41
+ # create a batch that you want to send
42
+ batch = Emarsys::Broadcast::Batch.new
43
+ batch.sender = 'sender_id'
44
+ batch.name = 'newsletter_2013_06_01'
45
+ batch.subject = 'June 2013 company news'
46
+ batch.body_html = '<h1>Dear 朋友!</h1>'
47
+ batch.recipients_path = '/path/to/your/csv/with/emails'
48
+
49
+ # create API client
50
+ api = Emarsys::Broadcast::API.new
51
+
52
+ # now send your batch
53
+ api.send_batch(batch)
54
+ ```
55
+
56
+ This will synchronously send the batch email to all recipients found in CSV file.
57
+
58
+ ### Moving batch properties to configuration
59
+
60
+ If you find yourself using same batch attributes over and over again, for example `recipients_path`,
61
+ you can move those values into configuration:
62
+
63
+ ```ruby
64
+ Emarsys::Broadcast::configure do |c|
65
+ c.api_user = your_api_user
66
+ c.api_password = your_api_password
67
+
68
+ c.sftp_user = your_sftp_user
69
+ c.sftp_password = your_sftp_password
70
+
71
+ c.sender = 'sender_id'
72
+ c.sender_domain = 'mail.your.company.com'
73
+ c.recipients_path = '/path/to/hyour/csv/with/emails'
74
+ end
75
+
76
+ # now you can omit these attributes when constructing a batch:
77
+ batch = Emarsys::Broadcast::Batch.new
78
+ batch.name = 'newsletter_2013_06_01'
79
+ batch.subject = 'June 2013 company news'
80
+ batch.body_html = '<h1>Dear 朋友!</h1>'
81
+
82
+ # send your batch as above, via api
83
+ ```
84
+
85
+
86
+ ### Creating batch from hash
87
+
88
+ If you like, you can construct your batch from a hash, like follows:
89
+
90
+ ```ruby
91
+ batch = Emarsys::Broadcast::Batch.new name: 'name', subject: 'subject', body_html: '<h1>html body</h1>'
92
+ ```
93
+
94
+ ### Batch name requirements
95
+
96
+ Batch name must be a valid identifier, i.e. start with a letter and contain letters, digits and underscores.
97
+ Emarsys requires every batch to have a unique name, but you don't have to maintain the uniqueness, because
98
+ this library internally appends a timestamp to each batch name before submitting it to Emarsys.
99
+
100
+ ### Batch subject requirements
101
+
102
+ Batch subject must be a string with a maximum length of 255 characters
103
+
104
+ ### Batch body html
105
+
106
+ Batch body html can be any HTML text, no restrictions
107
+
108
+ ### Batch body in plain text
109
+
110
+ It is possible to supply the body contents in plain text, this will broaden compatibility,
111
+ because some email clients don't support HTML and will download textual version instead.
112
+
113
+ ```ruby
114
+ batch = Emarsys::Broadcast::Batch.new
115
+ batch.name = 'newsletter_2013_06_01'
116
+ batch.subject = 'June 2013 company news'
117
+ batch.body_html = '<h1>Dear 朋友!</h1>'
118
+ batch.body_text = 'Dear 朋友'
119
+ ```
120
+
121
+ ### Batch validation
122
+
123
+ Emarsys::Broadcast uses ActiveModel for validating plain ruby objects, so you have all the methods for
124
+ validation you're accustomed to:
125
+
126
+ ```ruby
127
+ batch = Emarsys::Broadcast::Batch.new
128
+ batch.name = 'newsletter_2013_06_01'
129
+
130
+ batch.valid? # false
131
+ batch.errors
132
+ batch.errors.full_messages
133
+ ```
134
+
135
+ You can always validate your batch before submitting it.
136
+
137
+ Note that calling api#send_batch on an invalid batch will throw ValidationException
138
+
139
+ ```ruby
140
+ batch = get_invalid_batch
141
+ api = Emarsys::Broadcast::API.new
142
+ begin
143
+ api.send_batch batch
144
+ rescue Emarsys::Broadcast::ValidationException => ex
145
+ # get exception message
146
+ puts ex.message
147
+ # get exception errors (ActiveModel compatible)
148
+ puts ex.errors
149
+ end
150
+ ```
151
+
152
+ ### CSV file requirements
153
+
154
+ The recipients must be placed in a `UTF-8` CSV file.
155
+ The file must have at least one column with `EMAIL` header, for example:
156
+
157
+ ```csv
158
+ EMAIL
159
+ john.doe@gmail.com
160
+ sara.parker@yahoo.com
161
+ ...
162
+ ...
163
+ ```
164
+
165
+ If you use additional customization columns, add them to your CSV file:
166
+
167
+ ```csv
168
+ EMAIL FIRST_NAME
169
+ john.doe@gmail.com John
170
+ sara.parker@yahoo.com Sara
171
+ ...
172
+ ...
173
+ ```
174
+ Having additional columns allows you to customize your body_html, for example:
175
+
176
+ ```html
177
+ <h1>Hi, $$FIRST_NAME$$</h1>
178
+ ```
179
+
180
+ ### Batch sender_id requirements
181
+
182
+ Emarsys requires that API users maintain a list of possible senders, and restricts
183
+ sending emails from arbitrary sender.
184
+
185
+ To use any `sender_id` in your batch, create it first:
186
+
187
+ ```ruby
188
+ # assuming you have API configured already
189
+ api = Emarsys::Broadcast::API.new
190
+ # sender requires 3 arguments: id, name, email_address
191
+ sender = Emarsys::Broadcast::Sender.new('primary_newsletter_sender', 'My company', 'news@company.com')
192
+ api.create_sender sender
193
+ ```
194
+
195
+ Once you upload a sender, you can use its ID in any batch:
196
+
197
+ ```ruby
198
+ batch.sender_id = 'primary_newsletter_sender'
199
+ # more attributes
200
+ ```
201
+
202
+ ### Working with senders
203
+
204
+ #### Getting a full list of senders
205
+
206
+ ```ruby
207
+ api.get_senders
208
+ # returns Sender array
209
+ ```
210
+
211
+ #### Getting a single sender by email
212
+
213
+ ```ruby
214
+ api.get_sender('news@mycompany.ru')
215
+ ```
216
+
217
+ #### Find if a sender exists by email
218
+
219
+ ```ruby
220
+ api.sender_exists? 'news@mycompany.ru'
221
+ ```
222
+
223
+ ### Scheduling batches
224
+
225
+ By default a new batch is scheduled for immediate sending, but you can set the `send_time`
226
+
227
+ ```ruby
228
+ # Assuming using ActiveSupport and want to schedule a batch to be sent in 10 days
229
+ batch.send_time = Time.zone.now + 10.days
230
+ # .. more attributes
231
+
232
+ api.send_batch batch
233
+ ```
234
+
235
+ ### Compatibility
236
+
237
+ This gem is tested on
238
+ * MRI `1.9.2`, `1.9.3`, `2.0.0`
239
+ * JRuby 1.9 mode
240
+
241
+
242
+ ### Further plans
243
+
244
+ This library does not yet cover all Emarsys functionality, so the plans are to cover 100% of Emarsys features,
245
+ add async support, more scheduling options etc.
246
+
247
+ If you want to help me with this, pull requests are especially welcome :)
248
+
249
+
250
+ ## Contributing
251
+
252
+ 1. Fork it
253
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
254
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
255
+ 4. Run specs (`rspec`)
256
+ 5. Push to the branch (`git push origin my-new-feature`)
257
+ 6. Create new Pull Request
258
+
259
+
260
+ ## License
261
+
262
+
263
+ Copyright (c) 2013 Valentin Vasilyev
264
+
265
+ MIT License
266
+
267
+ Permission is hereby granted, free of charge, to any person obtaining
268
+ a copy of this software and associated documentation files (the
269
+ "Software"), to deal in the Software without restriction, including
270
+ without limitation the rights to use, copy, modify, merge, publish,
271
+ distribute, sublicense, and/or sell copies of the Software, and to
272
+ permit persons to whom the Software is furnished to do so, subject to
273
+ the following conditions:
274
+
275
+ The above copyright notice and this permission notice shall be
276
+ included in all copies or substantial portions of the Software.
277
+
278
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
279
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
280
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
281
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
282
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
283
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
284
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+
6
+ task(default: :spec)
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'emarsys/broadcast/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "emarsys-broadcast"
8
+ spec.version = Emarsys::Broadcast::VERSION
9
+ spec.authors = ["Valentin Vasilyev"]
10
+ spec.email = ["iamvalentin@gmail.com"]
11
+ spec.description = %q{Emarsys broadcast API for Ruby}
12
+ spec.summary = %q{Emarsys broadcast API for Ruby}
13
+ spec.homepage = "https://github.com/Valve/emarsys-broadcast-ruby"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "net-sftp"
22
+ spec.add_dependency "nokogiri"
23
+ spec.add_dependency "activemodel", "~> 3.0"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec", "~> 2.11"
28
+ spec.add_development_dependency "timecop"
29
+ spec.add_development_dependency "webmock"
30
+ end
@@ -0,0 +1,78 @@
1
+ module Emarsys
2
+ module Broadcast
3
+ class API
4
+
5
+ def initialize
6
+ @config = Emarsys::Broadcast.configuration
7
+ @sftp = SFTP.new @config
8
+ @http = HTTP.new @config
9
+ @xml_builder = XmlBuilder.new
10
+ end
11
+
12
+ def send_batch(batch)
13
+ batch = supplement_batch_from_config(batch)
14
+ validate_batch(batch)
15
+ validate_sender(batch.sender)
16
+ create_batch(batch)
17
+ upload_recipients(batch.recipients_path)
18
+ trigger_import(batch)
19
+ end
20
+
21
+ def create_batch(batch)
22
+ emarsys_sender = get_sender(batch.sender)
23
+ batch.sender_id = emarsys_sender.id
24
+ batch_xml = BatchXmlBuilder.new.build(batch)
25
+ @http.post("#{@config.api_base_path}/batches/#{batch.name}", batch_xml)
26
+ end
27
+
28
+ def upload_recipients(recipients_path)
29
+ @sftp.upload_file(recipients_path, File.basename(recipients_path))
30
+ end
31
+
32
+ def trigger_import(batch)
33
+ import_xml = XmlBuilder.new.import_xml(File.basename(batch.recipients_path))
34
+ @http.post("#{@config.api_base_path}/batches/#{batch.name}/import", import_xml)
35
+ end
36
+
37
+ def get_senders
38
+ response = @http.get("#{@config.api_base_path}/senders")
39
+ Nokogiri::XML(response).xpath('//sender').map do |node|
40
+ Sender.new(node.attr('id'), node.xpath('name').text, node.xpath('address').text)
41
+ end
42
+ end
43
+
44
+ def get_sender(email)
45
+ get_senders.find{|s| s.address == email}
46
+ end
47
+
48
+ def create_sender(sender)
49
+ sender_xml = @xml_builder.sender_xml(sender)
50
+ @http.put("#{@config.api_base_path}/senders/#{sender.id}", sender_xml)
51
+ end
52
+
53
+ def sender_exists?(email)
54
+ get_senders.any?{|s|s.address == email}
55
+ end
56
+
57
+ private
58
+
59
+ def supplement_batch_from_config(batch)
60
+ batch.recipients_path ||= @config.recipients_path
61
+ batch.send_time ||= Time.now
62
+ batch.sender ||= @config.sender
63
+ batch.sender_domain ||= @config.sender_domain
64
+ batch
65
+ end
66
+
67
+ def validate_batch(batch)
68
+ raise ValidationError.new('Batch is invalid', batch.errors.full_messages) unless batch.valid?
69
+ end
70
+
71
+ def validate_sender(email)
72
+ msg = 'This email is not registered with Emarsys as a sender, register it with `create_sender` api call'
73
+ raise ValidationError, msg, [msg] unless sender_exists? email
74
+ end
75
+ end
76
+ class ApiError < StandardError; end
77
+ end
78
+ end
@@ -0,0 +1,25 @@
1
+ require 'uri'
2
+ module Emarsys
3
+ module Broadcast
4
+ class Batch
5
+ include ActiveModel::Validations
6
+ attr_accessor \
7
+ :name,
8
+ :subject,
9
+ :body_html,
10
+ :body_text,
11
+ :recipients_path,
12
+ :send_time,
13
+ :sender,
14
+ :sender_domain,
15
+ :sender_id
16
+
17
+ validates :name, :subject, :body_html, :recipients_path, :sender, :sender_domain, presence: true
18
+ validates :name, format: {with: /^[^\d\W]\w*\z/i, message: 'must start with a letter and contain only letters, numbers and underscores'}
19
+ validates :subject, length: {maximum: 255}
20
+ validates :sender, format: {with: /@/, message: 'is not a valid email'}
21
+ validates :sender_domain, format: {with: URI::REL_URI, message: 'is not valid'}
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ require 'nokogiri'
2
+ module Emarsys
3
+ module Broadcast
4
+ class BatchXmlBuilder
5
+
6
+ def build(batch)
7
+ raise ArgumentError, 'batch is required' unless batch
8
+ builder = Nokogiri::XML::Builder.new do |xml|
9
+ xml.batch {
10
+ xml.name batch.name
11
+ xml.runDate format_time(batch.send_time)
12
+ xml.properties {
13
+ xml.property(key: :Sender){xml.text batch.sender_id}
14
+ xml.property(key: :Language){xml.text 'en'}
15
+ xml.property(key: :Encoding){xml.text 'UTF-8'}
16
+ xml.property(key: :Domain){xml.text batch.sender_domain}
17
+ }
18
+ xml.subject batch.subject
19
+ xml.html batch.body_html
20
+ xml.text batch.body_text
21
+ }
22
+ end
23
+ builder.to_xml
24
+ end
25
+
26
+
27
+ private
28
+
29
+ def format_time(time)
30
+ time.strftime("%Y-%m-%dT%H:%M:%S%z")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Emarsys
2
+ module Broadcast
3
+ class ConfigurationError < StandardError; end
4
+
5
+
6
+ class Configuration
7
+ attr_accessor \
8
+ :sftp_host,
9
+ :sftp_user,
10
+ :sftp_password,
11
+ :sftp_port,
12
+ :api_host,
13
+ :api_base_path,
14
+ :api_user,
15
+ :api_password,
16
+ :api_port,
17
+ :api_timeout,
18
+ :sender,
19
+ :sender_domain,
20
+ :recipients_path
21
+
22
+
23
+ def initialize
24
+ @sftp_host = 'e3.emarsys.net'
25
+ @sftp_port = 22
26
+ @api_host = 'e3.emarsys.net'
27
+ @api_base_path = '/bmapi/v2'
28
+ @api_port = 80
29
+ @api_timeout = 600 #10 minutes
30
+ end
31
+ end
32
+
33
+ class << self
34
+ attr_accessor :configuration
35
+ end
36
+
37
+ def self.configure
38
+ self.configuration ||= Configuration.new
39
+ yield configuration
40
+ configuration
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,11 @@
1
+ module Emarsys
2
+ module Broadcast
3
+ module Email
4
+ REGEX = /\A(|(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6})\z/i
5
+
6
+ def self.validate(email)
7
+ email =~ REGEX
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ require 'net/http'
2
+ module Emarsys
3
+ module Broadcast
4
+ class HTTP
5
+ include Validation
6
+
7
+ def initialize(config)
8
+ validate_config config
9
+ @config = config
10
+ end
11
+
12
+ def post(path, xml)
13
+ request(path, xml, :post)
14
+ end
15
+
16
+ def put(path, xml)
17
+ request(path, xml, :put)
18
+ end
19
+
20
+ def get(path)
21
+ request(path, nil, :get)
22
+ end
23
+
24
+
25
+ private
26
+
27
+ def request(path, data, method)
28
+ https = Net::HTTP.new(@config.api_host, Net::HTTP.https_default_port)
29
+ https.read_timeout = @config.api_timeout
30
+ https.use_ssl = true
31
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
32
+
33
+ https.start do |http|
34
+ case method.downcase.to_sym
35
+ when :post
36
+ req = Net::HTTP::Post.new(path)
37
+ when :put
38
+ req = Net::HTTP::Put.new(path)
39
+ when :get
40
+ req = Net::HTTP::Get.new(path)
41
+ end
42
+ req.basic_auth(@config.api_user, @config.api_password)
43
+ req.body = data
44
+ req.content_type = "application/xml"
45
+
46
+ res = http.request(req)
47
+
48
+ case res
49
+ when Net::HTTPSuccess
50
+ puts "OK"
51
+ return res.body
52
+ else
53
+ puts res.body
54
+ res.error!
55
+ end
56
+ end
57
+ end
58
+
59
+ def validate_config(config)
60
+ raise ConfigurationError, 'configuration is nil, did you forget to configure the gem?' unless config
61
+ raise ConfigurationError, 'api_host must be configured' unless string_present? config.api_host
62
+ raise ConfigurationError, 'api_user must be configured' unless string_present? config.api_user
63
+ raise ConfigurationError, 'api_password must be configured' unless string_present? config.api_password
64
+ unless within_range? config.api_port, 1..65535
65
+ raise ConfigurationError, 'api_port must be integer between 1 and 65535'
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,11 @@
1
+ module Emarsys
2
+ module Broadcast
3
+ class Sender
4
+ attr_reader :id, :name, :address
5
+
6
+ def initialize(id, name, address)
7
+ @id, @name, @address = id, name, address
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ require 'net/sftp'
2
+ module Emarsys
3
+ module Broadcast
4
+ class SFTP
5
+ include Validation
6
+
7
+ def initialize(config)
8
+ validate_config config
9
+ @config = config
10
+ end
11
+
12
+ def upload_file(local_path, remote_path)
13
+ Net::SFTP.start(@config.sftp_host, @config.sftp_user, password: @config.sftp_password) do |sftp|
14
+ sftp.upload!(local_path, remote_path)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def validate_config(config)
21
+ raise ConfigurationError, 'configuration is nil, did you forget to configure the gem?' unless config
22
+ raise ConfigurationError, 'sftp_host must be configured' unless string_present? config.sftp_host
23
+ raise ConfigurationError, 'sftp_user must be configured' unless string_present? config.sftp_user
24
+ raise ConfigurationError, 'sftp_password must be configured' unless string_present? config.sftp_password
25
+ unless within_range? config.sftp_port, 1..65535
26
+ raise ConfigurationError, 'sftp_port must be integer between 1 and 65535'
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end