mail_spy 0.0.2 → 0.1.1

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 (56) hide show
  1. data/README.rdoc +86 -24
  2. data/app/assets/images/mail_spy/background_tile.gif +0 -0
  3. data/app/assets/images/mail_spy/mailspy_sprite.png +0 -0
  4. data/app/assets/images/mail_spy/spy.ico +0 -0
  5. data/app/assets/javascripts/mail_spy/lib/backbone.js +1256 -0
  6. data/app/assets/javascripts/mail_spy/lib/underscore.js +999 -0
  7. data/app/assets/javascripts/mail_spy/reports.js +2 -0
  8. data/app/assets/javascripts/mail_spy/sendgrid.js +2 -0
  9. data/app/assets/stylesheets/mail_spy/application.css +62 -1
  10. data/app/assets/stylesheets/mail_spy/reports.css.scss +128 -0
  11. data/app/assets/stylesheets/mail_spy/sendgrid.css +4 -0
  12. data/app/controllers/mail_spy/reports_controller.rb +29 -0
  13. data/app/controllers/mail_spy/sendgrid_controller.rb +26 -0
  14. data/app/controllers/mail_spy/tracking_controller.rb +19 -11
  15. data/app/helpers/mail_spy/email_helper.rb +21 -3
  16. data/app/helpers/mail_spy/reports_helper.rb +4 -0
  17. data/app/helpers/mail_spy/sendgrid_helper.rb +4 -0
  18. data/app/mailers/mail_spy/core_mailer.rb +15 -18
  19. data/app/models/mail_spy/email.rb +127 -19
  20. data/app/models/mail_spy/process_log.rb +25 -0
  21. data/app/views/layouts/mail_spy/application.html.erb +3 -0
  22. data/app/views/mail_spy/reports/campaigns.html.erb +138 -0
  23. data/config/routes.rb +6 -0
  24. data/lib/generators/mail_spy/templates/mail_spy.rb +25 -1
  25. data/lib/mail_spy/manager.rb +100 -54
  26. data/lib/mail_spy/sendgrid/smtp_api_header.rb +68 -0
  27. data/lib/mail_spy/version.rb +1 -1
  28. data/lib/mail_spy.rb +39 -5
  29. data/test/dummy/app/views/email_templates/helper_test.html.erb +15 -0
  30. data/test/dummy/app/views/email_templates/helper_test.text.erb +11 -0
  31. data/test/dummy/config/environments/development.rb +1 -1
  32. data/test/dummy/config/environments/production.rb +5 -1
  33. data/test/dummy/config/environments/test.rb +4 -1
  34. data/test/dummy/config/initializers/mail_spy.rb +42 -0
  35. data/test/dummy/config/routes.rb +1 -2
  36. data/test/dummy/db/schema.rb +16 -0
  37. data/test/dummy/db/seeds.rb +16 -13
  38. data/test/dummy/lib/tasks/emails.rake +8 -0
  39. data/test/dummy/log/development.log +782 -0
  40. data/test/dummy/log/test.log +9952 -0
  41. data/test/fixtures/mail_spy/process_logs.yml +15 -0
  42. data/test/functional/mail_spy/core_mailer_test.rb +16 -9
  43. data/test/functional/mail_spy/reports_controller_test.rb +9 -0
  44. data/test/functional/mail_spy/sendgrid_controller_test.rb +9 -0
  45. data/test/functional/mail_spy/tracking_controller_test.rb +16 -11
  46. data/test/test_email_credentials.yml +2 -2
  47. data/test/test_email_credentials.yml.sample +0 -8
  48. data/test/test_helper.rb +15 -41
  49. data/test/unit/helpers/mail_spy/reports_helper_test.rb +6 -0
  50. data/test/unit/helpers/mail_spy/sendgrid_helper_test.rb +6 -0
  51. data/test/unit/mail_spy/manager_test.rb +101 -122
  52. data/test/unit/mail_spy/{email_template_test.rb → process_log_test.rb} +1 -1
  53. metadata +122 -34
  54. data/app/models/mail_spy/email_template.rb +0 -17
  55. data/test/fixtures/mail_spy/email_templates.yml +0 -9
  56. /data/lib/generators/mail_spy/{initialize_generator.rb → initializer_generator.rb} +0 -0
@@ -0,0 +1,2 @@
1
+ // Place all the behaviors and hooks related to the matching controller here.
2
+ // All this logic will automatically be available in application.js.
@@ -0,0 +1,2 @@
1
+ // Place all the behaviors and hooks related to the matching controller here.
2
+ // All this logic will automatically be available in application.js.
@@ -9,5 +9,66 @@
9
9
  * compiled file, but it's generally better to create a new file per style scope.
10
10
  *
11
11
  *= require_self
12
- *= require_tree .
12
+ *= require_tree
13
13
  */
14
+
15
+ /*************************************** Reset Styles */
16
+
17
+ html, body, div, span, applet, object, iframe,
18
+ h1, h2, h3, h4, h5, h6, p, blockquote, pre,
19
+ a, abbr, acronym, address, big, cite, code,
20
+ del, dfn, em, img, ins, kbd, q, s, samp,
21
+ small, strike, strong, sub, sup, tt, var,
22
+ b, u, i, center,
23
+ dl, dt, dd, ol, ul, li,
24
+ fieldset, form, label, legend,
25
+ table, caption, tbody, tfoot, thead, tr, th, td,
26
+ article, aside, canvas, details, embed,
27
+ figure, figcaption, footer, header, hgroup,
28
+ menu, nav, output, ruby, section, summary,
29
+ time, mark, audio, video {
30
+ margin: 0;
31
+ padding: 0;
32
+ border: 0;
33
+ font-size: 100%;
34
+ font: inherit;
35
+ vertical-align: baseline;
36
+ }
37
+ /* HTML5 display-role reset for older browsers */
38
+ article, aside, details, figcaption, figure,
39
+ footer, header, hgroup, menu, nav, section {
40
+ display: block;
41
+ }
42
+ body {
43
+ line-height: 1;
44
+ }
45
+ ol, ul {
46
+ list-style: none;
47
+ }
48
+ blockquote, q {
49
+ quotes: none;
50
+ }
51
+ blockquote:before, blockquote:after,
52
+ q:before, q:after {
53
+ content: '';
54
+ content: none;
55
+ }
56
+ table {
57
+ border-collapse: collapse;
58
+ border-spacing: 0;
59
+ }
60
+
61
+ /*************************************** End Reset */
62
+ /*************************************** New Global Styles */
63
+
64
+ body{
65
+ font-family: 'Spinnaker', sans-serif;
66
+ color:#fff;
67
+ background-image: url(background_tile.gif);
68
+ }
69
+ h1{
70
+ font-size:24px;
71
+ }
72
+ .Deemph{
73
+ font-size:.72em;
74
+ }
@@ -0,0 +1,128 @@
1
+ /*
2
+ Place all the styles related to the matching controller here.
3
+ They will automatically be included in application.css.
4
+ */
5
+ $colorHighlight:#80ff7a;
6
+ $colorMedlight:#214d30;
7
+ $colorSemiLowlight:#c3c3c3;
8
+ $colorLowlight:#3e4041;
9
+ $colorHighlightBad:#62222a;
10
+ $colorMedlightBad:#663137;
11
+
12
+
13
+
14
+ ._active{
15
+ color:$colorHighlight;
16
+ }
17
+
18
+ /**************************** Sprites for mailspy_sprite.png */
19
+ .MsSprite{
20
+ background-image:url(mailspy_sprite.png);
21
+ background-repeat:no-repeat;
22
+ display:inline-block;
23
+ *display:inline; zoom:1;
24
+ vertical-align:middle;
25
+ text-indent:-9999px;
26
+
27
+ &.Logo{
28
+ background-position:0 0;
29
+ width:180px;
30
+ height:50px;
31
+ }
32
+
33
+ &.Nav{
34
+ width: 27px;
35
+ height: 34px;
36
+ background-position-x:-50px;
37
+
38
+ &.Campaign{
39
+ background-position-y:-100px;
40
+ }
41
+ &.History{
42
+ background-position-y:-150px;
43
+ }
44
+ &.Template{
45
+ background-position-y:-200px;
46
+ }
47
+ &.Menu{
48
+ background-position-y:-250px;
49
+ }
50
+
51
+ &._active{
52
+ background-position-x:0;
53
+ }
54
+ &:hover{
55
+ background-position-x:-100px;
56
+ }
57
+ }
58
+
59
+ &.People{
60
+ background-position: -200px 0px;
61
+ height:60px;
62
+ width:80px;
63
+ }
64
+ &.Mail{
65
+ background-position: -200px -100px;
66
+ height:60px;
67
+ width:80px;
68
+ }
69
+ &.Pointer{
70
+ background-position: -200px -200px;
71
+ height:60px;
72
+ width:80px;
73
+ }
74
+ &.Money{
75
+ background-position: -200px -300px;
76
+ height:60px;
77
+ width:80px;
78
+ }
79
+ }
80
+
81
+ /**************************** End mailspy_sprite.png*/
82
+ /**************************** Report Components */
83
+ .KpiContainer{
84
+ margin:1em;
85
+ }
86
+ .Kpi{
87
+ display:inline-block;
88
+ *display:inline; zoom:1;
89
+ width:200px;
90
+ text-align:center;
91
+
92
+ .Icon{
93
+ display:block;
94
+ margin:8px auto;
95
+ }
96
+ kpi{
97
+ color:$colorMedlight;
98
+ font-size:1.2em;
99
+ margin:.2em;
100
+ }
101
+ figure{
102
+ color:$colorHighlight;
103
+ font-size:1.8em;
104
+ margin:.2em;
105
+ }
106
+ }
107
+
108
+ .CampaignContainer{
109
+ background-color:rgba(0,0,0,.4);
110
+ padding:.2em 1em 1em 1em;
111
+ margin:2em 0;
112
+ }
113
+
114
+ .FunnelContainer{
115
+ padding:.2em 1em 1em 1em;
116
+ margin:2em 0;
117
+ thead{
118
+ td{
119
+ color:$colorLowlight;
120
+ text-align:left;
121
+ }
122
+ }
123
+ td{
124
+ padding:.5em 2em;
125
+ color:$colorHighlight;
126
+ text-align:left;
127
+ }
128
+ }
@@ -0,0 +1,4 @@
1
+ /*
2
+ Place all the styles related to the matching controller here.
3
+ They will automatically be included in application.css.
4
+ */
@@ -0,0 +1,29 @@
1
+ module MailSpy
2
+ class ReportsController < ApplicationController
3
+
4
+ def campaigns
5
+
6
+ end
7
+
8
+ def emails
9
+
10
+ end
11
+
12
+ def email
13
+ @email = MailSpy.create_email({
14
+ :campaign => "2012-valentines",
15
+ :stream => "a-yikes",
16
+ :component => "a",
17
+ :schedule_at => Date.today()+1, # Should never be used
18
+ :subject => "Just a test",
19
+ :template_values => {},
20
+ :from => "EMAIL PREVIEW",
21
+ :reply_to => "NOREPLY",
22
+ :to => "nowhere"
23
+ })
24
+ @email[:cache_bust] = true
25
+ render :text=>@email.parsed_html_content, :html_safe => true
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ module MailSpy
2
+ class SendgridController < ApplicationController
3
+
4
+ #Parses a notification from the sendgrid system
5
+ def notification
6
+ #Must be passed in via SendGrids SMTP API Headers
7
+ email_id = params['eid']
8
+ return head 400 if email_id.blank? || params[:event].blank?
9
+
10
+ #Send grid event name
11
+ event_action = "sendgrid-#{params[:event]}"
12
+
13
+ case params[:event]
14
+ when 'bounce', 'dropped'
15
+ MailSpy.track_action(email_id, event_action, {:reason => params[:reason]})
16
+ when 'click'
17
+ MailSpy.track_action(email_id, event_action, {:url => params[:url]})
18
+ when 'deferred', 'delivered', 'processed', 'open', 'spamreport', 'unsubscribe'
19
+ MailSpy.track_action(email_id, event_action)
20
+ end
21
+
22
+ head :ok
23
+ end
24
+
25
+ end
26
+ end
@@ -14,7 +14,22 @@ module MailSpy
14
14
  }
15
15
  )
16
16
 
17
- redirect_to params[:url]
17
+
18
+ # Add in the GA tokens if present
19
+
20
+ ga_hash = {}
21
+ ga_hash[:utm_source] = email.utm_source if email.utm_source.present?
22
+ ga_hash[:utm_medium] = email.utm_medium if email.utm_medium.present?
23
+ ga_hash[:utm_campaign] = email.utm_campaign if email.utm_campaign.present?
24
+ ga_hash[:utm_term] = email.utm_term if email.utm_term.present?
25
+ ga_hash[:utm_content] = email.utm_content if email.utm_content.present?
26
+
27
+ #TODO this is not fool proof, it breaks on bad urls (like missing http://)
28
+ #TODO this also breaks when a param is already present it isn;t smart
29
+ uri = URI.parse(params[:url])
30
+ uri.query = [uri.query, ga_hash.to_param].compact.join('&')
31
+
32
+ redirect_to uri.to_s
18
33
  end
19
34
 
20
35
  def bug
@@ -33,20 +48,13 @@ module MailSpy
33
48
  def action
34
49
  email_id = params[:eid]
35
50
  action_type = params[:action_type]
36
- details = params[:details]
51
+ count = params[:count] || 1
52
+ details = params[:details] || {}
37
53
 
38
54
  head :unprocessable_entity and return if email_id.blank? || action_type.blank?
39
55
  head :unprocessable_entity and return if details.present? && !details.kind_of?(Hash)
40
56
 
41
- hash ={}
42
- hash[:action_type] = action_type
43
- hash[:count] = params[:count] if !params[:count].nil?
44
- hash[:details] = details if details.present? && details.kind_of?(Hash)
45
-
46
- # Save it up
47
- email = MailSpy::Email.find(email_id)
48
- email.actions.create!(hash)
49
-
57
+ MailSpy.track_action(email_id, action_type, details, count)
50
58
  head 200
51
59
  end
52
60
  end
@@ -1,3 +1,5 @@
1
+ require 'uri'
2
+
1
3
  module MailSpy
2
4
  module EmailHelper
3
5
 
@@ -21,9 +23,18 @@ module MailSpy
21
23
  tag_options = tag_options(html_options)
22
24
 
23
25
  # Inject our tracking url, and pass in the redirect_url
24
- url = url_for(:controller => "mail_spy/tracking", :action => :link, :url => url,
25
- :n => @_track_count, :eid => @_email_id)
26
26
 
27
+ hash = {
28
+ :controller => "mail_spy/tracking",
29
+ :action => :link,
30
+ :url => url,
31
+ :only_path => false,
32
+ :host => MailSpy.tracker_host,
33
+ :n => @_track_count,
34
+ :eid => @_email.id
35
+ }
36
+
37
+ url = url_for(hash)
27
38
 
28
39
  href_attr = "href=\"#{ERB::Util.html_escape(url)}\"" unless href
29
40
  "<a #{href_attr}#{tag_options}>#{ERB::Util.html_escape(name || url)}</a>".html_safe
@@ -33,7 +44,14 @@ module MailSpy
33
44
 
34
45
  # Support for open tracking, client support, etc
35
46
  def tracking_bug
36
- "<img src='#{url_for(:controller => "mail_spy/tracking", :action => :bug, :eid => @_email_id)}' />".html_safe
47
+ url = url_for(
48
+ :controller => "mail_spy/tracking",
49
+ :action => :bug,
50
+ :eid => @_email.id,
51
+ :only_path => false,
52
+ :host => MailSpy.tracker_host
53
+ )
54
+ "<img src='#{url}' style='display:none' width='1' height='1' border='0' />".html_safe
37
55
  end
38
56
 
39
57
  end
@@ -0,0 +1,4 @@
1
+ module MailSpy
2
+ module ReportsHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module MailSpy
2
+ module SendgridHelper
3
+ end
4
+ end
@@ -4,31 +4,28 @@ module MailSpy
4
4
 
5
5
  def template(email)
6
6
  # Slight hack to provide information to helpers
7
- @_email_id = email.id
7
+ @_email = email
8
8
  @template_values = email.template_values
9
-
10
- # Set Headers
9
+ esp = MailSpy.esps[email.email_service_provider]
11
10
  email_hash = {}
12
- std_email_keys = [:to, :cc, :bcc, :from, :subject, :message_id, :sender, :reply_to]
13
- std_email_keys.each { |key| set_if_present(email_hash,email, key) }
14
- headers.merge!(email.headers) if email.headers.present?
15
11
 
16
- # Content of the email
17
- html_erb = email.email_template.html_erb || ""
18
- text_erb = email.email_template.html_erb || ""
12
+ # Evaluate the subject line as erb
13
+ if email.subject.present?
14
+ subject = ERB.new(email.subject).result(binding)
15
+ email_hash[:subject] = subject
16
+ end
17
+
18
+ # Set Headers
19
+ std_email_keys = [:to, :cc, :bcc, :from, :message_id, :sender, :reply_to]
20
+ std_email_keys.each { |key| set_if_present(email_hash, email, key) }
21
+ email.headers.each {|key, value| headers[key] = value} if email.headers.present?
19
22
 
23
+ # Create the mail message
20
24
  mail_message = mail(email_hash) do |format|
21
- format.text { render :inline => text_erb }
22
- format.html { render :inline => html_erb }
25
+ format.text { render :inline => email.text_erb }
26
+ format.html { render :inline => email.html_erb }
23
27
  end
24
28
 
25
- # Email Service provider setup
26
- # TODO Only support smtp at the moment
27
- raise "No Email service providers installed" unless MailSpy.esps.present?
28
- esp = MailSpy.esps[email.email_service_provider]
29
- esp_key = MailSpy.esps.keys[rand(MailSpy.esps.keys.count)]
30
- esp = MailSpy.esps[esp_key] if esp.blank?
31
-
32
29
  mail_message.delivery_method.settings.merge!(
33
30
  {
34
31
  :address => esp.address,
@@ -3,6 +3,8 @@ module MailSpy
3
3
  include Mongoid::Document
4
4
  include Mongoid::Timestamps
5
5
 
6
+ @@template_cache = {}
7
+
6
8
  # Standard Email options
7
9
  field :to
8
10
  field :cc
@@ -11,16 +13,15 @@ module MailSpy
11
13
  field :subject
12
14
  field :sender #Sets envelope from (aka sender header)
13
15
  field :reply_to
14
- field :headers
16
+ field :headers, :type => Hash, :default => {}
15
17
  field :message_id #Sets the unique id for each email
16
18
 
17
19
  # Email content
18
- field :template_name, :type => String
19
- field :template_values, :type => Hash
20
- belongs_to :email_template, :class_name => 'MailSpy::EmailTemplate'
20
+ field :template_values, :type => Hash, :default => {}
21
21
 
22
22
  # Support for tracking which esp ran the email
23
23
  field :email_service_provider, :type => String
24
+ validates_presence_of :email_service_provider
24
25
 
25
26
  # References back to a user in another db
26
27
  field :user_id, :type => Integer
@@ -32,39 +33,146 @@ module MailSpy
32
33
 
33
34
  # Scheduling and completion
34
35
  field :schedule_at, :type => DateTime
35
- field :sent, :type => Boolean
36
+ field :sent, :type => Boolean, :default => false
36
37
  index :schedule_at
38
+ index :sent
39
+
40
+ # Error reporting
41
+ field :failed, :type => Boolean, :default => false
42
+ field :error_message, :type => String
43
+ field :error_backtrace, :type => Array
44
+ index :failed
37
45
 
38
46
  # Record keeping of what happened
39
47
  embeds_many :actions, :class_name => "MailSpy::Action"
40
48
 
49
+ # Google Analytics tracking
50
+ field :utm_source, :type => String
51
+ field :utm_medium, :type => String
52
+ field :utm_campaign, :type => String
53
+ field :utm_term, :type => String
54
+ field :utm_content, :type => String
55
+
41
56
 
42
57
  def template_values
43
58
  self.read_attribute(:template_values).try(:with_indifferent_access)
44
59
  end
45
60
 
46
-
47
- def email_parts
48
- template = self.email_template
49
- html_erb = template.html_erb
50
- text_erb = template.text_erb
51
- mail = MailSpy::DummyMailer.template(html_erb, text_erb, self.template_values)
52
- return {
53
- :html => mail.html_part.body.to_s,
54
- :text => mail.text_part.body.to_s
55
- }
56
- end
57
-
58
61
  # Great for debugging emails
59
62
  def parsed_html_content
60
- return MailSpy::CoreMailer.template(self).text_part.body.to_s
63
+ return MailSpy::CoreMailer.template(self).html_part.body.to_s
61
64
  end
62
65
 
63
66
  def parsed_text_content
64
- return MailSpy::CoreMailer.template(self).html_part.body.to_s
67
+ return MailSpy::CoreMailer.template(self).text_part.body.to_s
68
+ end
69
+
70
+ def html_erb
71
+ load_template("html.erb")
72
+ end
73
+
74
+ def text_erb
75
+ load_template("text.erb")
76
+ end
77
+
78
+ def self.deliver_batch(current_time, start_id, offset, total, step=200, num_threads=100)
79
+ wq = WorkQueue.new(num_threads, step*2)
80
+ processed = 0
81
+ while true
82
+ emails = self.where(
83
+ :schedule_at.lte => current_time,
84
+ :sent => false,
85
+ :failed => false,
86
+ :_id.gt => start_id
87
+ ).limit(step).offset(offset)
88
+
89
+ break if total >= processed || emails.blank?
90
+ emails.each do |email|
91
+ wq.enqueue_b do
92
+ email.deliver
93
+ end
94
+ end
95
+ processed += emails.count
96
+ end
97
+ wq.join
98
+ processed
99
+ end
100
+
101
+ def deliver
102
+ begin
103
+ MailSpy::CoreMailer.template(self).deliver
104
+ self.update_attribute(:sent, true)
105
+ rescue Exception => e
106
+ self.failed = true
107
+ self.error_message = e.try(:message)
108
+ self.error_backtrace = e.try(:backtrace)
109
+ self.save!
110
+ end
111
+ end
112
+
113
+
114
+ protected
115
+
116
+ # Loads a template file from s3 using
117
+ def load_template(suffix)
118
+ raise "No aws_campaign_bucket_set" if MailSpy.aws_campaign_bucket.strip.blank?
119
+
120
+ # Check the cache using our convention
121
+ path = "#{self.campaign}/#{self.stream}/#{self.component}.#{suffix}"
122
+ return @@template_cache[path] if @@template_cache[path].present? && !self[:cache_bust]
123
+
124
+ # Load the object from s3
125
+ s3 = AWS::S3.new(:access_key_id => MailSpy.aws_access_key_id,
126
+ :secret_access_key => MailSpy.aws_secret_access_key)
127
+ campaign_bucket = s3.buckets[MailSpy.aws_campaign_bucket]
128
+ campaign_bucket = buckets.create(MailSpy.aws_campaign_bucket) unless campaign_bucket.exists?
129
+ object = campaign_bucket.objects[path]
130
+ raise "no object found at path: #{path}" unless object.exists?
131
+
132
+ # Read and return
133
+ @@template_cache[path] = (object.read || "")
134
+ return @@template_cache[path]
65
135
  end
66
136
 
67
137
 
138
+ def self.generate_subject_reports
139
+ map = <<-eof
140
+ function(){
141
+ var action_counts = {};
142
+ for(var i=0, len = this.actions.length; i < len; i++){
143
+ var action = this.actions[i];
144
+ action_counts[action.action_type] = action_counts[action.action_type] || 0;
145
+ action_counts[action.action_type]++;
146
+ }
147
+
148
+ emit({
149
+ campaign:this.campaign,
150
+ stream:this.stream,
151
+ component:this.component,
152
+ subject:this.subject
153
+ }, action_counts);
154
+ }
155
+ eof
156
+
157
+ reduce = <<-eof
158
+ function(key, values){
159
+ var result = {};
160
+
161
+ values.forEach(function(hash){
162
+ for(var key in hash){
163
+ result[key] = result[key] || 0
164
+ result[key] += hash[key]
165
+ }
166
+ });
167
+
168
+ return result;
169
+ }
170
+ eof
171
+
172
+ results = collection.map_reduce(map, reduce, :out => "test_reports")
173
+
174
+ end
175
+
68
176
  def self.generate_campaign_stats
69
177
 
70
178
  map = <<-eof
@@ -0,0 +1,25 @@
1
+ module MailSpy
2
+ class ProcessLog
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+
6
+ field :start, :type => Time
7
+ field :end, :type => Time
8
+ field :seconds_elapsed, :type => Integer
9
+ field :success, :type => Boolean, :default => false
10
+ field :running, :type => Boolean, :default => false
11
+
12
+
13
+ def self.currently_processing?
14
+ self.where(:running => true).first.present?
15
+ end
16
+
17
+ # Deletes logs after a given date if no date is given we default
18
+ # to logs that are older that 3 months.
19
+ def self.delete_stale_logs(date=nil)
20
+ date = Time.now.advance(:months => -3) if date.nil?
21
+ self.where("created_at < #{date}").delete_all
22
+ end
23
+
24
+ end
25
+ end
@@ -2,8 +2,11 @@
2
2
  <html>
3
3
  <head>
4
4
  <title>MailSpy</title>
5
+ <link rel="icon" href="/assets/mail_spy/spy.ico" type="image/x-icon">
6
+ <link rel="shortcut icon" href="/assets/mail_spy/spy.ico" type="image/x-icon">
5
7
  <%= stylesheet_link_tag "mail_spy/application", :media => "all" %>
6
8
  <%= javascript_include_tag "mail_spy/application" %>
9
+ <link href='http://fonts.googleapis.com/css?family=Spinnaker' rel='stylesheet' type='text/css'>
7
10
  <%= csrf_meta_tags %>
8
11
  </head>
9
12
  <body>