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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/Readme.md +3 -12
  4. data/ditty.gemspec +4 -1
  5. data/exe/ditty +4 -0
  6. data/lib/ditty/cli.rb +44 -0
  7. data/lib/ditty/controllers/application.rb +3 -4
  8. data/lib/ditty/controllers/component.rb +38 -14
  9. data/lib/ditty/controllers/main.rb +115 -34
  10. data/lib/ditty/controllers/users.rb +7 -10
  11. data/lib/ditty/db.rb +6 -1
  12. data/lib/ditty/emails/base.rb +74 -0
  13. data/lib/ditty/emails/forgot_password.rb +15 -0
  14. data/lib/ditty/helpers/authentication.rb +2 -2
  15. data/lib/ditty/helpers/component.rb +7 -4
  16. data/lib/ditty/helpers/views.rb +5 -2
  17. data/lib/ditty/listener.rb +41 -7
  18. data/lib/ditty/models/user.rb +8 -4
  19. data/lib/ditty/policies/identity_policy.rb +2 -2
  20. data/lib/ditty/rake_tasks.rb +6 -5
  21. data/lib/ditty/services/authentication.rb +55 -0
  22. data/lib/ditty/services/email.rb +18 -20
  23. data/lib/ditty/services/logger.rb +10 -8
  24. data/lib/ditty/services/pagination_wrapper.rb +82 -0
  25. data/lib/ditty/services/settings.rb +45 -0
  26. data/lib/ditty/version.rb +1 -1
  27. data/migrate/20180307_password_reset.rb +10 -0
  28. data/views/audit_logs/index.haml +1 -1
  29. data/views/emails/base.haml +2 -0
  30. data/views/emails/forgot_password.haml +26 -0
  31. data/views/emails/layouts/action.haml +68 -0
  32. data/views/emails/layouts/alert.haml +88 -0
  33. data/views/emails/layouts/billing.haml +108 -0
  34. data/views/identity/forgot.haml +16 -0
  35. data/views/identity/login.haml +16 -5
  36. data/views/identity/register.haml +8 -0
  37. data/views/identity/reset.haml +20 -0
  38. data/views/layout.haml +2 -0
  39. metadata +62 -5
  40. data/lib/ditty/helpers/wisper.rb +0 -14
@@ -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
- def method_missing(method, *args, &block)
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 respond_to_missing?(method, _include_private = false)
19
- Mail.respond_to? method
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 config
23
- @config ||= symbolize_keys File.exist?(CONFIG) ? YAML.load_file(CONFIG) : default
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
- cfg = config
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 symbolize_keys(hash)
40
- return hash.map { |v| symbolize_keys(v) } if hash.is_a? Array
41
- return hash unless hash.is_a? Hash
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.each do |values|
19
- klass = values['class'].constantize
20
- opts = tr(values['options']) || nil
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['level']
23
- logger.level = klass.const_get(values['level'].to_sym)
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
- @config ||= File.exist?(CONFIG) ? YAML.load_file(CONFIG) : default
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
- [{ 'name' => 'default', 'class' => 'Logger' }]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ditty
4
- VERSION = '0.4.1'.freeze
4
+ VERSION = '0.6.0'.freeze
5
5
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ alter_table :identities do
6
+ add_column :reset_token, String
7
+ add_column :reset_requested, DateTime
8
+ end
9
+ end
10
+ end
@@ -16,7 +16,7 @@
16
16
  %tr
17
17
  %td
18
18
  -if entity.user
19
- %a{ href: "/users/#{entity.user.id}" }= entity.user.email
19
+ %a{ href: "#{settings.map_path}/users/#{entity.user.id}" }= entity.user.email
20
20
  -else
21
21
  None
22
22
  %td
@@ -0,0 +1,2 @@
1
+ - if defined? content
2
+ = content
@@ -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"}