ditty 0.4.1 → 0.6.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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/Readme.md +3 -12
- data/ditty.gemspec +4 -1
- data/exe/ditty +4 -0
- data/lib/ditty/cli.rb +44 -0
- data/lib/ditty/controllers/application.rb +3 -4
- data/lib/ditty/controllers/component.rb +38 -14
- data/lib/ditty/controllers/main.rb +115 -34
- data/lib/ditty/controllers/users.rb +7 -10
- data/lib/ditty/db.rb +6 -1
- data/lib/ditty/emails/base.rb +74 -0
- data/lib/ditty/emails/forgot_password.rb +15 -0
- data/lib/ditty/helpers/authentication.rb +2 -2
- data/lib/ditty/helpers/component.rb +7 -4
- data/lib/ditty/helpers/views.rb +5 -2
- data/lib/ditty/listener.rb +41 -7
- data/lib/ditty/models/user.rb +8 -4
- data/lib/ditty/policies/identity_policy.rb +2 -2
- data/lib/ditty/rake_tasks.rb +6 -5
- data/lib/ditty/services/authentication.rb +55 -0
- data/lib/ditty/services/email.rb +18 -20
- data/lib/ditty/services/logger.rb +10 -8
- data/lib/ditty/services/pagination_wrapper.rb +82 -0
- data/lib/ditty/services/settings.rb +45 -0
- data/lib/ditty/version.rb +1 -1
- data/migrate/20180307_password_reset.rb +10 -0
- data/views/audit_logs/index.haml +1 -1
- data/views/emails/base.haml +2 -0
- data/views/emails/forgot_password.haml +26 -0
- data/views/emails/layouts/action.haml +68 -0
- data/views/emails/layouts/alert.haml +88 -0
- data/views/emails/layouts/billing.haml +108 -0
- data/views/identity/forgot.haml +16 -0
- data/views/identity/login.haml +16 -5
- data/views/identity/register.haml +8 -0
- data/views/identity/reset.haml +20 -0
- data/views/layout.haml +2 -0
- metadata +62 -5
- data/lib/ditty/helpers/wisper.rb +0 -14
data/lib/ditty/services/email.rb
CHANGED
@@ -1,45 +1,43 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'mail'
|
4
|
+
require 'active_support/inflector'
|
4
5
|
require 'ditty/services/logger'
|
6
|
+
require 'ditty/services/settings'
|
5
7
|
|
6
8
|
module Ditty
|
7
9
|
module Services
|
8
10
|
module Email
|
9
|
-
CONFIG = './config/email.yml'.freeze
|
10
|
-
|
11
11
|
class << self
|
12
|
-
|
13
|
-
return super unless respond_to_missing?(method)
|
14
|
-
config!
|
15
|
-
Mail.send(method, *args, &block)
|
16
|
-
end
|
12
|
+
include ActiveSupport::Inflector
|
17
13
|
|
18
|
-
def
|
19
|
-
|
14
|
+
def config!
|
15
|
+
cfg = config
|
16
|
+
Mail.defaults do
|
17
|
+
delivery_method cfg[:delivery_method].to_sym, (cfg[:options] || {})
|
18
|
+
end
|
20
19
|
end
|
21
20
|
|
22
|
-
def
|
23
|
-
|
21
|
+
def deliver(email, to = nil, options = {})
|
22
|
+
config!
|
23
|
+
options[:to] ||= to unless to.nil?
|
24
|
+
email = from_symbol(email, options) if email.is_a? Symbol
|
25
|
+
email.deliver!
|
24
26
|
end
|
25
27
|
|
26
28
|
private
|
27
29
|
|
28
|
-
def config
|
29
|
-
|
30
|
-
Mail.defaults do
|
31
|
-
delivery_method cfg[:delivery_method].to_sym, (cfg[:options] || {})
|
32
|
-
end
|
30
|
+
def config
|
31
|
+
default.merge Ditty::Services::Settings.values(:email) || {}
|
33
32
|
end
|
34
33
|
|
35
34
|
def default
|
36
35
|
{ delivery_method: :logger, logger: Ditty::Services::Logger.instance }
|
37
36
|
end
|
38
37
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
Hash[hash.map { |k, v| [k.to_sym, symbolize_keys(v)] }]
|
38
|
+
def from_symbol(email, options)
|
39
|
+
require "ditty/emails/#{email}"
|
40
|
+
constantize("Ditty::Emails::#{classify(email)}").new(options)
|
43
41
|
end
|
44
42
|
end
|
45
43
|
end
|
@@ -4,23 +4,25 @@ require 'logger'
|
|
4
4
|
require 'yaml'
|
5
5
|
require 'singleton'
|
6
6
|
require 'active_support/inflector'
|
7
|
+
require 'ditty/services/settings'
|
8
|
+
require 'active_support/core_ext/object/blank'
|
7
9
|
|
8
10
|
module Ditty
|
9
11
|
module Services
|
10
12
|
class Logger
|
11
13
|
include Singleton
|
12
14
|
|
13
|
-
CONFIG = './config/logger.yml'.freeze
|
14
15
|
attr_reader :loggers
|
15
16
|
|
16
17
|
def initialize
|
17
18
|
@loggers = []
|
18
|
-
config.
|
19
|
-
|
20
|
-
|
19
|
+
return if config[:loggers].blank?
|
20
|
+
config[:loggers].each do |values|
|
21
|
+
klass = values[:class].constantize
|
22
|
+
opts = tr(values[:options]) || nil
|
21
23
|
logger = klass.new(opts)
|
22
|
-
if values[
|
23
|
-
logger.level = klass.const_get(values[
|
24
|
+
if values[:level]
|
25
|
+
logger.level = klass.const_get(values[:level].to_sym)
|
24
26
|
end
|
25
27
|
@loggers << logger
|
26
28
|
end
|
@@ -37,7 +39,7 @@ module Ditty
|
|
37
39
|
private
|
38
40
|
|
39
41
|
def config
|
40
|
-
|
42
|
+
default.merge Ditty::Services::Settings.values(:logger) || {}
|
41
43
|
end
|
42
44
|
|
43
45
|
def tr(val)
|
@@ -48,7 +50,7 @@ module Ditty
|
|
48
50
|
end
|
49
51
|
|
50
52
|
def default
|
51
|
-
[{
|
53
|
+
{ loggers: [{ name: 'default', class: 'Logger' }] }
|
52
54
|
end
|
53
55
|
end
|
54
56
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Ditty
|
2
|
+
module Services
|
3
|
+
class PaginationWrapper
|
4
|
+
attr_reader :list
|
5
|
+
|
6
|
+
def initialize(list)
|
7
|
+
@list = list
|
8
|
+
end
|
9
|
+
|
10
|
+
def last_page?
|
11
|
+
if list.respond_to? :'last_page?'
|
12
|
+
list.last_page?
|
13
|
+
else
|
14
|
+
list.current_page == list.total_pages
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def first_page?
|
19
|
+
if list.respond_to? :'first_page?'
|
20
|
+
list.first_page?
|
21
|
+
else
|
22
|
+
list.current_page == 0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def prev_page
|
27
|
+
if list.respond_to? :prev_page
|
28
|
+
list.prev_page
|
29
|
+
else
|
30
|
+
list.previous_page
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def page_count
|
35
|
+
if list.respond_to? :page_count
|
36
|
+
list.page_count
|
37
|
+
else
|
38
|
+
list.total_pages
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def page_size
|
43
|
+
if list.respond_to? :page_size
|
44
|
+
list.page_size
|
45
|
+
else
|
46
|
+
list.per_page
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def pagination_record_count
|
51
|
+
if list.respond_to? :pagination_record_count
|
52
|
+
list.pagination_record_count
|
53
|
+
else
|
54
|
+
list.total_entries
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def method_missing(method, *args)
|
59
|
+
return super unless respond_to_missing?(method)
|
60
|
+
|
61
|
+
list.send(method, *args)
|
62
|
+
end
|
63
|
+
|
64
|
+
def respond_to_missing?(method, _include_private = false)
|
65
|
+
list.respond_to? method
|
66
|
+
end
|
67
|
+
|
68
|
+
def current_page_record_range
|
69
|
+
if list.respond_to? :current_page_record_range
|
70
|
+
list.current_page_record_range
|
71
|
+
else
|
72
|
+
return (0..0) if list.current_page > page_count
|
73
|
+
|
74
|
+
a = 1 + (list.current_page - 1) * page_size
|
75
|
+
b = a + page_size - 1
|
76
|
+
b = pagination_record_count if b > pagination_record_count
|
77
|
+
a..b
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
require 'active_support/core_ext/hash/deep_merge'
|
6
|
+
require 'active_support/core_ext/hash/keys'
|
7
|
+
|
8
|
+
module Ditty
|
9
|
+
module Services
|
10
|
+
module Settings
|
11
|
+
CONFIG_FOLDER = './config'.freeze
|
12
|
+
CONFIG_FILE = "#{CONFIG_FOLDER}/settings.yml".freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def [](key)
|
16
|
+
values(key.to_sym)
|
17
|
+
end
|
18
|
+
|
19
|
+
def values(scope = :settings)
|
20
|
+
@values ||= begin
|
21
|
+
v = Hash.new do |h, k|
|
22
|
+
h[k] = if File.file?("#{CONFIG_FOLDER}/#{k}.yml")
|
23
|
+
read("#{CONFIG_FOLDER}/#{k}.yml")
|
24
|
+
elsif k != :settings && h[:settings].key?(k)
|
25
|
+
h[:settings][k]
|
26
|
+
end
|
27
|
+
h[k]
|
28
|
+
end
|
29
|
+
v[:settings] = File.file?(CONFIG_FILE) ? read(CONFIG_FILE) : {}
|
30
|
+
v
|
31
|
+
end
|
32
|
+
@values[scope]
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_writer :values
|
36
|
+
|
37
|
+
def read(filename)
|
38
|
+
base = YAML.safe_load(ERB.new(File.read(filename)).result).deep_symbolize_keys
|
39
|
+
base.deep_merge!(base[ENV['APP_ENV'].to_sym]) unless ENV['APP_ENV'].nil? || base[ENV['APP_ENV'].to_sym].nil?
|
40
|
+
base
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/ditty/version.rb
CHANGED
data/views/audit_logs/index.haml
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
%p
|
2
|
+
Hi #{identity.user.name}
|
3
|
+
|
4
|
+
%p
|
5
|
+
We received a request to reset the password for #{identity.username}.
|
6
|
+
You can use the button below to complete this request.
|
7
|
+
%strong
|
8
|
+
This reset is only valid for 24 hours
|
9
|
+
|
10
|
+
%p.text-center
|
11
|
+
%a{ href: reset_url }
|
12
|
+
|
13
|
+
%p
|
14
|
+
For security purposes, if you did not request this password reset,
|
15
|
+
please contact support.
|
16
|
+
|
17
|
+
%p
|
18
|
+
Thanx!
|
19
|
+
|
20
|
+
%ul
|
21
|
+
%li
|
22
|
+
Requesting IP:
|
23
|
+
= request.ip
|
24
|
+
%li
|
25
|
+
Requesting User Agent:
|
26
|
+
= request.user_agent
|
@@ -0,0 +1,68 @@
|
|
1
|
+
!!!
|
2
|
+
%html{:style => "font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;", :xmlns => "http://www.w3.org/1999/xhtml"}
|
3
|
+
%head
|
4
|
+
%meta{:content => "width=device-width", :name => "viewport"}/
|
5
|
+
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
|
6
|
+
%title Actionable emails e.g. reset password
|
7
|
+
:css
|
8
|
+
img {
|
9
|
+
max-width: 100%;
|
10
|
+
}
|
11
|
+
body {
|
12
|
+
-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
|
13
|
+
}
|
14
|
+
body {
|
15
|
+
background-color: #f6f6f6;
|
16
|
+
}
|
17
|
+
@media only screen and (max-width: 640px) {
|
18
|
+
body {
|
19
|
+
padding: 0 !important;
|
20
|
+
}
|
21
|
+
h1 {
|
22
|
+
font-weight: 800 !important; margin: 20px 0 5px !important;
|
23
|
+
}
|
24
|
+
h2 {
|
25
|
+
font-weight: 800 !important; margin: 20px 0 5px !important;
|
26
|
+
}
|
27
|
+
h3 {
|
28
|
+
font-weight: 800 !important; margin: 20px 0 5px !important;
|
29
|
+
}
|
30
|
+
h4 {
|
31
|
+
font-weight: 800 !important; margin: 20px 0 5px !important;
|
32
|
+
}
|
33
|
+
h1 {
|
34
|
+
font-size: 22px !important;
|
35
|
+
}
|
36
|
+
h2 {
|
37
|
+
font-size: 18px !important;
|
38
|
+
}
|
39
|
+
h3 {
|
40
|
+
font-size: 16px !important;
|
41
|
+
}
|
42
|
+
.container {
|
43
|
+
padding: 0 !important; width: 100% !important;
|
44
|
+
}
|
45
|
+
.content {
|
46
|
+
padding: 0 !important;
|
47
|
+
}
|
48
|
+
.content-wrap {
|
49
|
+
padding: 10px !important;
|
50
|
+
}
|
51
|
+
.invoice {
|
52
|
+
width: 100% !important;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
%body{:bgcolor => "#f6f6f6", :itemscope => "", :itemtype => "http://schema.org/EmailMessage", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;"}
|
56
|
+
%table.body-wrap{:bgcolor => "#f6f6f6", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;"}
|
57
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
58
|
+
%td{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;", :valign => "top"}
|
59
|
+
%td.container{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;", :valign => "top", :width => "600"}
|
60
|
+
.content{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;"}
|
61
|
+
= content
|
62
|
+
- if defined? footer
|
63
|
+
.footer{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;"}
|
64
|
+
%table{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;", :width => "100%"}
|
65
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
66
|
+
%td.aligncenter.content-block{:align => "center", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;", :valign => "top"}
|
67
|
+
= footer
|
68
|
+
%td{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;", :valign => "top"}
|
@@ -0,0 +1,88 @@
|
|
1
|
+
!!!
|
2
|
+
%html{:style => "font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;", :xmlns => "http://www.w3.org/1999/xhtml"}
|
3
|
+
%head
|
4
|
+
%meta{:content => "width=device-width", :name => "viewport"}/
|
5
|
+
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
|
6
|
+
%title Alerts e.g. approaching your limit
|
7
|
+
:css
|
8
|
+
img {
|
9
|
+
max-width: 100%;
|
10
|
+
}
|
11
|
+
body {
|
12
|
+
-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
|
13
|
+
}
|
14
|
+
body {
|
15
|
+
background-color: #f6f6f6;
|
16
|
+
}
|
17
|
+
@media only screen and (max-width: 640px) {
|
18
|
+
body {
|
19
|
+
padding: 0 !important;
|
20
|
+
}
|
21
|
+
h1 {
|
22
|
+
font-weight: 800 !important; margin: 20px 0 5px !important;
|
23
|
+
}
|
24
|
+
h2 {
|
25
|
+
font-weight: 800 !important; margin: 20px 0 5px !important;
|
26
|
+
}
|
27
|
+
h3 {
|
28
|
+
font-weight: 800 !important; margin: 20px 0 5px !important;
|
29
|
+
}
|
30
|
+
h4 {
|
31
|
+
font-weight: 800 !important; margin: 20px 0 5px !important;
|
32
|
+
}
|
33
|
+
h1 {
|
34
|
+
font-size: 22px !important;
|
35
|
+
}
|
36
|
+
h2 {
|
37
|
+
font-size: 18px !important;
|
38
|
+
}
|
39
|
+
h3 {
|
40
|
+
font-size: 16px !important;
|
41
|
+
}
|
42
|
+
.container {
|
43
|
+
padding: 0 !important; width: 100% !important;
|
44
|
+
}
|
45
|
+
.content {
|
46
|
+
padding: 0 !important;
|
47
|
+
}
|
48
|
+
.content-wrap {
|
49
|
+
padding: 10px !important;
|
50
|
+
}
|
51
|
+
.invoice {
|
52
|
+
width: 100% !important;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
%body{:bgcolor => "#f6f6f6", :itemscope => "", :itemtype => "http://schema.org/EmailMessage", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;"}
|
56
|
+
%table.body-wrap{:bgcolor => "#f6f6f6", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;"}
|
57
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
58
|
+
%td{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;", :valign => "top"}
|
59
|
+
%td.container{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;", :valign => "top", :width => "600"}
|
60
|
+
.content{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;"}
|
61
|
+
%table.main{:bgcolor => "#fff", :cellpadding => "0", :cellspacing => "0", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;", :width => "100%"}
|
62
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
63
|
+
%td.alert.alert-warning{:align => "center", :bgcolor => "#FF9F00", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background-color: #FF9F00; margin: 0; padding: 20px;", :valign => "top"}
|
64
|
+
Warning: You're approaching your limit. Please upgrade.
|
65
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
66
|
+
%td.content-wrap{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;", :valign => "top"}
|
67
|
+
%table{:cellpadding => "0", :cellspacing => "0", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;", :width => "100%"}
|
68
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
69
|
+
%td.content-block{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;", :valign => "top"}
|
70
|
+
You have
|
71
|
+
%strong{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"} 1 free report
|
72
|
+
remaining.
|
73
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
74
|
+
%td.content-block{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;", :valign => "top"}
|
75
|
+
Add your credit card now to upgrade your account to a premium plan to ensure you don't miss out on any reports.
|
76
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
77
|
+
%td.content-block{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;", :valign => "top"}
|
78
|
+
%a.btn-primary{:href => "http://www.mailgun.com", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;"} Upgrade my account
|
79
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
80
|
+
%td.content-block{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;", :valign => "top"}
|
81
|
+
Thanks for choosing Acme Inc.
|
82
|
+
.footer{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;"}
|
83
|
+
%table{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;", :width => "100%"}
|
84
|
+
%tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
|
85
|
+
%td.aligncenter.content-block{:align => "center", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;", :valign => "top"}
|
86
|
+
%a{:href => "http://www.mailgun.com", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;"} Unsubscribe
|
87
|
+
from these alerts.
|
88
|
+
%td{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;", :valign => "top"}
|