ditty 0.4.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"}
|