howitzer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/Gemfile.lock +113 -0
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/Rakefile +2 -0
- data/bin/howitzer +41 -0
- data/generators/config/config_generator.rb +24 -0
- data/generators/config/templates/cucumber.yml +11 -0
- data/generators/config/templates/custom.yml +4 -0
- data/generators/config/templates/default.yml +76 -0
- data/generators/cucumber/cucumber_generator.rb +31 -0
- data/generators/cucumber/templates/common_steps.rb +23 -0
- data/generators/cucumber/templates/env.rb +38 -0
- data/generators/cucumber/templates/example.feature +16 -0
- data/generators/cucumber/templates/transformers.rb +28 -0
- data/generators/emails/emails_generator.rb +23 -0
- data/generators/emails/templates/example_email.rb +7 -0
- data/generators/pages/pages_generator.rb +23 -0
- data/generators/pages/templates/example_menu.rb +14 -0
- data/generators/pages/templates/example_page.rb +15 -0
- data/generators/root/root_generator.rb +25 -0
- data/generators/root/templates/.gitignore +20 -0
- data/generators/root/templates/Gemfile +4 -0
- data/generators/root/templates/Rakefile +4 -0
- data/generators/root/templates/boot.rb +16 -0
- data/generators/tasks/tasks_generator.rb +22 -0
- data/generators/tasks/templates/cucumber.rake +57 -0
- data/howitzer.gemspec +36 -0
- data/lib/howitzer.rb +5 -0
- data/lib/howitzer/helpers.rb +58 -0
- data/lib/howitzer/init.rb +5 -0
- data/lib/howitzer/utils.rb +14 -0
- data/lib/howitzer/utils/capybara_patched.rb +22 -0
- data/lib/howitzer/utils/capybara_settings.rb +110 -0
- data/lib/howitzer/utils/data_generator/data_storage.rb +41 -0
- data/lib/howitzer/utils/data_generator/gen.rb +89 -0
- data/lib/howitzer/utils/email/email.rb +46 -0
- data/lib/howitzer/utils/email/mail_client.rb +126 -0
- data/lib/howitzer/utils/email/mailgun.rb +175 -0
- data/lib/howitzer/utils/email/mailgun_helper.rb +35 -0
- data/lib/howitzer/utils/locator_store.rb +118 -0
- data/lib/howitzer/utils/logger.rb +108 -0
- data/lib/howitzer/version.rb +3 -0
- data/lib/howitzer/web_page.rb +62 -0
- metadata +334 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'howitzer/utils/logger'
|
2
|
+
module CapybaraSettings
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def self.base_ff_profile_settings
|
6
|
+
profile = Selenium::WebDriver::Firefox::Profile.new
|
7
|
+
profile["network.http.phishy-userpass-length"] = 255
|
8
|
+
profile["browser.safebrowsing.malware.enabled"] = false
|
9
|
+
profile["network.automatic-ntlm-auth.allow-non-fqdn"] = true
|
10
|
+
profile["network.ntlm.send-lm-response"] = true
|
11
|
+
profile["network.automatic-ntlm-auth.trusted-uris"] = settings.app_main_host
|
12
|
+
profile
|
13
|
+
end
|
14
|
+
|
15
|
+
Capybara.run_server = false
|
16
|
+
Capybara.app_host = ''
|
17
|
+
Capybara.default_wait_time = settings.timeout_small
|
18
|
+
Capybara.ignore_hidden_elements = true
|
19
|
+
case settings.driver.to_sym
|
20
|
+
when :selenium
|
21
|
+
Capybara.register_driver :selenium do |app|
|
22
|
+
params = {browser: settings.sel_browser.to_sym}
|
23
|
+
params[:profile] = base_ff_profile_settings if ff_browser?
|
24
|
+
Capybara::Selenium::Driver.new app, params
|
25
|
+
end
|
26
|
+
when :selenium_dev
|
27
|
+
Capybara.register_driver :selenium_dev do |app|
|
28
|
+
profile = base_ff_profile_settings
|
29
|
+
fb_name = "firebug-#{settings.sel_firebug_version}-fx.xpi"
|
30
|
+
fp_name = "firepath-#{settings.sel_firepath_version}.xpi"
|
31
|
+
[fb_name, fp_name].each do |name|
|
32
|
+
profile.add_extension(File.expand_path(name, File.join('shared', 'vendor', 'firebug')))
|
33
|
+
end
|
34
|
+
profile['extensions.firebug.currentVersion'] = settings.sel_firebug_version # avoid 'first run' tab
|
35
|
+
profile["extensions.firebug.previousPlacement"] = 1
|
36
|
+
profile["extensions.firebug.onByDefault"] = true
|
37
|
+
profile["extensions.firebug.defaultPanelName"] = "firepath"
|
38
|
+
profile["extensions.firebug.script.enableSites"] = true
|
39
|
+
profile["extensions.firebug.net.enableSites"] = true
|
40
|
+
profile["extensions.firebug.console.enableSites"] = true
|
41
|
+
|
42
|
+
Capybara::Selenium::Driver.new app, browser: :firefox, profile: profile
|
43
|
+
end
|
44
|
+
when :webkit
|
45
|
+
require 'capybara-webkit'
|
46
|
+
when :poltergeist
|
47
|
+
require 'capybara/poltergeist'
|
48
|
+
when :sauce
|
49
|
+
caps_opts = {
|
50
|
+
platform: settings.sl_platform,
|
51
|
+
browser_name: settings.sl_browser_name,
|
52
|
+
name: 'Capybara test name should be overwritten',
|
53
|
+
"max-duration" => settings.sl_max_duration,
|
54
|
+
'idle-timeout' => settings.sl_idle_timeout,
|
55
|
+
'selenium-version' => settings.sl_selenium_version,
|
56
|
+
'record-screenshots' => settings.sl_record_screenshot,
|
57
|
+
'video-upload-on-pass' => settings.sl_video_upload_on_pass
|
58
|
+
}
|
59
|
+
|
60
|
+
unless (settings.sl_browser_version.to_s || "").empty?
|
61
|
+
caps_opts['browser-version'] = settings.sl_browser_version.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
options = {
|
65
|
+
url: settings.sl_url,
|
66
|
+
desired_capabilities: Selenium::WebDriver::Remote::Capabilities.new(caps_opts),
|
67
|
+
http_client: Selenium::WebDriver::Remote::Http::Default.new.tap{|c| c.timeout = settings.timeout_medium},
|
68
|
+
browser: :remote
|
69
|
+
}
|
70
|
+
|
71
|
+
Capybara.register_driver :sauce do |app|
|
72
|
+
Capybara::Selenium::Driver.new(app, options)
|
73
|
+
end
|
74
|
+
|
75
|
+
else
|
76
|
+
log.error "Unknown '#{settings.driver}' driver. Check your settings, it should be one of [selenium, selenium_dev, webkit, sauce]"
|
77
|
+
end
|
78
|
+
|
79
|
+
Capybara.default_driver = settings.driver.to_sym
|
80
|
+
Capybara.javascript_driver = settings.driver.to_sym
|
81
|
+
|
82
|
+
def sauce_resource_path(name)
|
83
|
+
host = "https://#{settings.sl_user}:#{settings.sl_api_key}@saucelabs.com"
|
84
|
+
path = "/rest/#{settings.sl_user}/jobs/#{session_id}/results/#{name}"
|
85
|
+
"#{host}#{path}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def update_sauce_job_status(json_data = {})
|
89
|
+
host = "http://#{settings.sl_user}:#{settings.sl_api_key}@saucelabs.com"
|
90
|
+
path = "/rest/v1/#{settings.sl_user}/jobs/#{session_id}"
|
91
|
+
url = "#{host}#{path}"
|
92
|
+
RestClient.put url, json_data.to_json, content_type: :json, accept: :json
|
93
|
+
end
|
94
|
+
|
95
|
+
def suite_name
|
96
|
+
res = if ENV['RAKE_TASK']
|
97
|
+
res = ENV['RAKE_TASK'].sub(/(?:r?spec|cucumber):?(.*)/, '\1').upcase
|
98
|
+
res.empty? ? 'ALL' : res
|
99
|
+
else
|
100
|
+
'CUSTOM'
|
101
|
+
end
|
102
|
+
"#{res} #{settings.sl_browser_name.upcase}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def session_id
|
106
|
+
Capybara.current_session.driver.browser.instance_variable_get(:@bridge).session_id
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module DataGenerator
|
2
|
+
|
3
|
+
module DataStorage
|
4
|
+
@data ||= {}
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def store(ns, key, value)
|
8
|
+
check_ns(ns)
|
9
|
+
@data[ns][key] = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def extract(ns, key=nil)
|
13
|
+
check_ns(ns)
|
14
|
+
key ? @data[ns][key] : @data[ns]
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear_ns(ns)
|
18
|
+
init_ns(ns)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def check_ns(ns)
|
24
|
+
if ns
|
25
|
+
init_ns(ns) if ns_absent?(ns)
|
26
|
+
else
|
27
|
+
raise 'Data storage namespace can not be empty'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def ns_absent?(ns)
|
32
|
+
!@data.key?(ns)
|
33
|
+
end
|
34
|
+
|
35
|
+
def init_ns(ns)
|
36
|
+
@data[ns] = {}
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module DataGenerator
|
2
|
+
module Gen
|
3
|
+
# examples:
|
4
|
+
# member = Gen::user #user.email: 'u<XXXX>@<settings.mail_pop3_domain>'
|
5
|
+
# member = Gen::user('user@test.com') #user.email: 'member@test.com'
|
6
|
+
# member = Gen::user(settings.def_test_user) #user.email: settings.def_test_user
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def user(params={})
|
10
|
+
prefix = serial
|
11
|
+
default = {
|
12
|
+
email: gen_user_email(prefix),
|
13
|
+
login: nil,
|
14
|
+
first_name: gen_first_name(prefix),
|
15
|
+
last_name: gen_last_name(prefix),
|
16
|
+
password: settings.def_test_pass
|
17
|
+
}
|
18
|
+
User.new(default.merge(params))
|
19
|
+
end
|
20
|
+
|
21
|
+
def given_user_by_number(num)
|
22
|
+
data = DataStorage.extract('user', num.to_i)
|
23
|
+
unless data
|
24
|
+
data = Gen::user
|
25
|
+
DataStorage.store('user', num.to_i, data)
|
26
|
+
end
|
27
|
+
data
|
28
|
+
end
|
29
|
+
|
30
|
+
def serial
|
31
|
+
a = [('a'..'z').to_a, (0..9).to_a].flatten.shuffle
|
32
|
+
"#{Time.now.utc.strftime("%j%H%M%S")}#{a[0..4].join}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_all_mailboxes
|
36
|
+
DataStorage.extract('user').each_value do |user|
|
37
|
+
user.delete_mailbox
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def gen_user_email(serial=nil)
|
44
|
+
"#{gen_user_name(serial)}@#{settings.mail_pop3_domain}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def gen_user_name(serial=nil)
|
48
|
+
gen_entity('u', serial)
|
49
|
+
end
|
50
|
+
|
51
|
+
def gen_first_name(serial=nil)
|
52
|
+
gen_entity('FirstName', serial)
|
53
|
+
end
|
54
|
+
|
55
|
+
def gen_last_name(serial=nil)
|
56
|
+
gen_entity('LastName', serial)
|
57
|
+
end
|
58
|
+
|
59
|
+
def gen_entity(prefix, serial)
|
60
|
+
"#{prefix}#{serial.nil? ? self.serial : serial}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class User < Object
|
65
|
+
|
66
|
+
attr_reader :login, :domain, :email, :password, :mailbox, :first_name, :last_name, :full_name
|
67
|
+
|
68
|
+
def initialize(params={})
|
69
|
+
@email = params.delete(:email)
|
70
|
+
@email_name, @domain = @email.to_s.split('@')
|
71
|
+
@login = params.delete(:login) || @email_name
|
72
|
+
@password = params.delete(:password)
|
73
|
+
@first_name = params.delete(:first_name)
|
74
|
+
@last_name = params.delete(:last_name)
|
75
|
+
@full_name = "#@first_name #@last_name"
|
76
|
+
@mailbox = params.delete(:mailbox)
|
77
|
+
end
|
78
|
+
|
79
|
+
def create_mailbox
|
80
|
+
@mailbox = MailClient.create_mailbox(@email_name) if settings.mail_pop3_domain == @domain
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete_mailbox
|
85
|
+
MailClient.delete_mailbox(@mailbox) unless @mailbox.nil?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rspec/matchers'
|
2
|
+
require 'howitzer/utils/email/mail_client'
|
3
|
+
|
4
|
+
class Email
|
5
|
+
include RSpec::Matchers
|
6
|
+
attr_reader :recipient_address
|
7
|
+
|
8
|
+
def initialize(message)
|
9
|
+
message.subject.should include(self.class::SUBJECT)
|
10
|
+
@recipient_address = ::Mail::Address.new(message.to.first)
|
11
|
+
@message = message
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.find_by_recipient(recipient)
|
15
|
+
find(recipient, self::SUBJECT)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.find(recipient, subject)
|
19
|
+
messages = MailClient.by_email(recipient).find_mail do |mail|
|
20
|
+
/#{Regexp.escape(subject)}/ === mail.subject && mail.to == [recipient]
|
21
|
+
end
|
22
|
+
|
23
|
+
if messages.first.nil?
|
24
|
+
log.error "#{self} was not found (recipient: '#{recipient}')"
|
25
|
+
messages.first.should_not be_nil
|
26
|
+
end
|
27
|
+
new(messages.first)
|
28
|
+
end
|
29
|
+
|
30
|
+
def plain_text_body
|
31
|
+
get_mime_part(@message, 'text/plain').to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_mime_part(part, type)
|
35
|
+
return part.body if part["content-type"].to_s =~ %r!#{type}!
|
36
|
+
# Recurse the multi-parts
|
37
|
+
part.parts.each do |sub_part|
|
38
|
+
r = get_mime_part(sub_part, type)
|
39
|
+
return r if r
|
40
|
+
end
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
protected :get_mime_part
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'net/pop'
|
2
|
+
require 'timeout'
|
3
|
+
require 'mail'
|
4
|
+
require 'howitzer/utils/email/mailgun_helper'
|
5
|
+
|
6
|
+
class MailClient
|
7
|
+
|
8
|
+
extend MailgunHelper
|
9
|
+
@clients = {}
|
10
|
+
|
11
|
+
def self.default
|
12
|
+
log.info "Connect to default mailbox"
|
13
|
+
options = self.merge_opts
|
14
|
+
@clients[options] = MailClient.send :new unless @clients.has_key?(options)
|
15
|
+
@clients[options]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.by_email(name)
|
19
|
+
log.info "Connect to '#{name}' mailbox"
|
20
|
+
options = self.merge_opts(:pop3 => {:user_name => name}, :smtp => {})
|
21
|
+
@clients[options] = MailClient.send :new, options unless @clients.has_key?(options)
|
22
|
+
@clients[options]
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_mail(max_wait = settings.mail_pop3_timeout, keep_or_delete = :delete, &block)
|
26
|
+
messages = []
|
27
|
+
time_of_start = Time.now
|
28
|
+
exception = nil
|
29
|
+
while Time.now < time_of_start + max_wait
|
30
|
+
begin
|
31
|
+
exception = nil
|
32
|
+
start do |pop_obj|
|
33
|
+
pop_obj.mails.each do |mail|
|
34
|
+
begin
|
35
|
+
mail_message = Mail.new(mail.pop.to_s)
|
36
|
+
if block_given?
|
37
|
+
if block.call(mail_message)
|
38
|
+
if keep_or_delete == :delete
|
39
|
+
mail.delete
|
40
|
+
log.info "Mail '#{mail_message.subject}' deleted from #{@options[:pop3][:user_name]}"
|
41
|
+
end
|
42
|
+
messages << mail_message
|
43
|
+
@old_ids << mail.unique_id
|
44
|
+
return messages
|
45
|
+
end
|
46
|
+
else
|
47
|
+
messages << mail_message
|
48
|
+
end
|
49
|
+
rescue Net::POPError => e
|
50
|
+
log.warn "Exception in POP find_mail: #{e.message}"
|
51
|
+
exception = e
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
sleep 5
|
57
|
+
rescue Errno::ETIMEDOUT, Errno::ECONNRESET, Net::POPAuthenticationError => e
|
58
|
+
log.warn "Exception in POP find_mail: #{e.message}. Will retry."
|
59
|
+
exception = e
|
60
|
+
end
|
61
|
+
end
|
62
|
+
log.error exception unless exception.nil?
|
63
|
+
messages
|
64
|
+
end
|
65
|
+
|
66
|
+
def send_mail(mail_from, mail_to, mail_subject, mail_body, mail_attachment=nil)
|
67
|
+
log.info "Send emails with next parameters:\n " +
|
68
|
+
"mail_from : #{mail_from},\n mail_to : #{mail_to}\n " +
|
69
|
+
"mail_subject : #{mail_subject},\n has_attachment? : #{!mail_attachment.nil?}"
|
70
|
+
outbound_mail = Mail.new do
|
71
|
+
from(mail_from)
|
72
|
+
subject(mail_subject)
|
73
|
+
body(mail_body)
|
74
|
+
add_file(mail_attachment) unless mail_attachment.nil?
|
75
|
+
end
|
76
|
+
outbound_mail[:to] = mail_to
|
77
|
+
|
78
|
+
retryable(:on => [Errno::ETIMEDOUT, Timeout::Error], :sleep => 10, :tries => 3, :trace => true, :logger => log) do
|
79
|
+
outbound_mail.deliver!
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def empty_inbox
|
84
|
+
begin
|
85
|
+
start {|pop_obj| pop_obj.delete_all }
|
86
|
+
log.info "Email box (#{@options[:pop3][:user_name]}) was purged"
|
87
|
+
rescue Net::POPError => e
|
88
|
+
log.warn "Exception during deletion all messages from '#{@options[:pop3][:user_name]}':\n#{e.message}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def start(&block)
|
93
|
+
retryable(:timeout => settings.mail_pop3_timeout, :sleep => 5, :trace => true, :logger => log) do
|
94
|
+
Net::POP3.start(@options[:pop3][:address], @options[:pop3][:port],
|
95
|
+
@options[:pop3][:user_name], @options[:pop3][:password], &block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def initialize(*arg)
|
101
|
+
@options = MailClient.merge_opts(*arg)
|
102
|
+
Net::POP3.enable_ssl(OpenSSL::SSL::VERIFY_NONE)
|
103
|
+
@old_ids = []
|
104
|
+
options = @options
|
105
|
+
::Mail.defaults{delivery_method :smtp, options[:smtp]}
|
106
|
+
@time_of_start = Time.now
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.merge_opts(opts={:smtp => {}, :pop3 => {}})
|
110
|
+
def_smtp_opts = {:address => settings.mail_smtp_server,
|
111
|
+
:port => settings.mail_smtp_port,
|
112
|
+
:domain => settings.mail_smtp_domain,
|
113
|
+
:user_name => settings.mail_smtp_user_name,
|
114
|
+
:password => settings.mail_smtp_user_pass,
|
115
|
+
:authentication => 'plain',
|
116
|
+
:enable_starttls_auto => true}
|
117
|
+
|
118
|
+
def_pop3_opts = {:address => settings.mail_pop3_server,
|
119
|
+
:port => settings.mail_pop3_port,
|
120
|
+
:user_name => settings.mail_pop3_user_name,
|
121
|
+
:password => settings.mail_pop3_user_pass}
|
122
|
+
{:smtp => def_smtp_opts.merge(opts[:smtp]),
|
123
|
+
:pop3 => def_pop3_opts.merge(opts[:pop3])}
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
#For details, please see https://github.com/mailgun/mailgun.rb or https://github.com/hardikshah/mailgun.rb
|
2
|
+
#Modified from original adding rest-client 'remove'
|
3
|
+
require 'active_resource'
|
4
|
+
require 'rest-client'
|
5
|
+
|
6
|
+
class Mailgun
|
7
|
+
|
8
|
+
# Initializes Mailgun API
|
9
|
+
# Must be called before any other method
|
10
|
+
#
|
11
|
+
# Parameters:
|
12
|
+
# api_key - Your API key
|
13
|
+
# api_url - API base URL
|
14
|
+
#
|
15
|
+
def self.init(api_key, api_url = "https://mailgun.net/api/")
|
16
|
+
MailgunResource.password = api_key
|
17
|
+
api_url = api_url.gsub(/\/$/, '') + "/"
|
18
|
+
MailgunResource.site = api_url
|
19
|
+
end
|
20
|
+
|
21
|
+
# This is a patch of private ActiveResource method.
|
22
|
+
# It takes HTTPResponse and raise AR-like error if response code is not 2xx
|
23
|
+
def self.handle_response(response)
|
24
|
+
case response.code.to_i
|
25
|
+
when 301,302
|
26
|
+
raise(Redirection.new(response))
|
27
|
+
when 200...400
|
28
|
+
response
|
29
|
+
when 400
|
30
|
+
raise(ActiveResource::BadRequest.new(response))
|
31
|
+
when 401
|
32
|
+
raise(ActiveResource::UnauthorizedAccess.new(response))
|
33
|
+
when 403
|
34
|
+
raise(ActiveResource::ForbiddenAccess.new(response))
|
35
|
+
when 404
|
36
|
+
raise(ActiveResource::ResourceNotFound.new(response))
|
37
|
+
when 405
|
38
|
+
raise(ActiveResource::MethodNotAllowed.new(response))
|
39
|
+
when 409
|
40
|
+
raise(ActiveResource::ResourceConflict.new(response))
|
41
|
+
when 410
|
42
|
+
raise(ActiveResource::ResourceGone.new(response))
|
43
|
+
when 422
|
44
|
+
raise(ActiveResource::ResourceInvalid.new(response))
|
45
|
+
when 401...500
|
46
|
+
raise(ActiveResource::ClientError.new(response))
|
47
|
+
when 500...600
|
48
|
+
raise(ActiveResource::ServerError.new(response))
|
49
|
+
else
|
50
|
+
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module RequestBuilder
|
55
|
+
def prepare_request(url_string)
|
56
|
+
uri = URI.parse(url_string)
|
57
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
58
|
+
http.use_ssl = true if uri.port == 443
|
59
|
+
return [http, (uri.path + '?' + uri.query)]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class MailgunMessage
|
65
|
+
extend Mailgun::RequestBuilder
|
66
|
+
|
67
|
+
MAILGUN_TAG = 'X-Mailgun-Tag'
|
68
|
+
|
69
|
+
# Sends a MIME-formatted message
|
70
|
+
#
|
71
|
+
# raw_mime =
|
72
|
+
# "Content-Type: text/plain;charset=utf-8\n" +
|
73
|
+
# "From: me@host\n" +
|
74
|
+
# "To: you@host\n" +
|
75
|
+
# "Subject: Hello!\n\n" +
|
76
|
+
# "Body"
|
77
|
+
# MailgunMessage::send_raw("me@host", "you@host", raw_mime)
|
78
|
+
#
|
79
|
+
def self.send_raw(sender, recipients, raw_body, servername='')
|
80
|
+
uri_str = "#{MailgunResource.site}messages.eml?api_key=#{MailgunResource.password}&servername=#{servername}"
|
81
|
+
http, url = prepare_request(uri_str)
|
82
|
+
data = "#{sender}\n#{recipients}\n\n#{raw_body}"
|
83
|
+
res = http.post(url, data, {"Content-type" => "text/plain" })
|
84
|
+
Mailgun::handle_response(res)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Sends a plain-text message
|
88
|
+
#
|
89
|
+
# MailgunMessage::send_text("me@host.com",
|
90
|
+
# "you@host.com",
|
91
|
+
# "Subject",
|
92
|
+
# "Hi!\nThis is message body")
|
93
|
+
#
|
94
|
+
def self.send_text(sender, recipients, subject, text, servername='', options = nil)
|
95
|
+
uri_str = "#{MailgunResource.site}messages.txt?api_key=#{MailgunResource.password}&servername=#{servername}"
|
96
|
+
params = { :sender => sender, :recipients => recipients, :subject => subject, :body => text}
|
97
|
+
unless options.nil?
|
98
|
+
params['options'] = ActiveSupport::JSON.encode(options)
|
99
|
+
end
|
100
|
+
http, url = prepare_request(uri_str)
|
101
|
+
req = Net::HTTP::Post.new(url)
|
102
|
+
req.set_form_data(params)
|
103
|
+
res = http.request(req)
|
104
|
+
Mailgun::handle_response(res)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Base class for Mailgun resource classes
|
109
|
+
# It adds upsert() method on top of ActiveResource::Base
|
110
|
+
#
|
111
|
+
class MailgunResource < ActiveResource::Base
|
112
|
+
self.user = "api_key"
|
113
|
+
extend Mailgun::RequestBuilder
|
114
|
+
|
115
|
+
# Create new resource or update it if resource already exist.
|
116
|
+
# There are 2 differences between upsert() and save():
|
117
|
+
# - Upsert does not raise an exception if a resource already exist.
|
118
|
+
# - Upsert does not return the id of freshly inserted resource
|
119
|
+
#
|
120
|
+
# >> route = Route.new
|
121
|
+
# >> route.pattern = '*@mydomain.com'
|
122
|
+
# >> route.destination = 'http://mydomain.com/addcomment'
|
123
|
+
# >> route.upsert()
|
124
|
+
#
|
125
|
+
def upsert()
|
126
|
+
self.class.post('upsert', {}, self.to_xml())
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# All mail arriving to emails addresses that have mailboxes associated
|
131
|
+
# will be stored on the server and can be later accessed via IMAP or POP3
|
132
|
+
# protocols.
|
133
|
+
#
|
134
|
+
# Mailbox has several properties:
|
135
|
+
#
|
136
|
+
# alex@gmail.com
|
137
|
+
# ^ ^
|
138
|
+
# | |
|
139
|
+
# user domain
|
140
|
+
#
|
141
|
+
class Mailbox < MailgunResource
|
142
|
+
# Example of a CSV file:
|
143
|
+
#
|
144
|
+
# john@domain.com, password
|
145
|
+
# doe@domain.com, password2
|
146
|
+
#
|
147
|
+
def self.upsert_from_csv(mailboxes)
|
148
|
+
uri_str = "#{MailgunResource.site}mailboxes.txt?api_key=#{MailgunResource.password}"
|
149
|
+
http, url = prepare_request(uri_str)
|
150
|
+
res = http.post(url, mailboxes, {"Content-type" => "text/plain" })
|
151
|
+
Mailgun::handle_response(res)
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.remove(mailbox)
|
155
|
+
res = RestClient.delete "https://api:#{MailgunResource.password}"\
|
156
|
+
"@api.mailgun.net/v2/#{mailbox.domain}/mailboxes/#{mailbox.user}"
|
157
|
+
Mailgun::handle_response(res)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
# This class represents Mailgun route.
|
163
|
+
# A route has 2 properties: pattern and destination
|
164
|
+
#
|
165
|
+
# Examples of patterns:
|
166
|
+
# - '*' - match all
|
167
|
+
# - exact match (foo@bar.com)
|
168
|
+
# - domain pattern, i.e. a pattern like "@example.com" - it will match all emails going to example.com
|
169
|
+
# - any regular expression
|
170
|
+
#
|
171
|
+
# Destination can be one of:
|
172
|
+
# - an emails address to forward to
|
173
|
+
# - a URL for HTTP POST
|
174
|
+
class Route < MailgunResource
|
175
|
+
end
|