homing 0.1.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.
- data/app/controllers/emails_controller.rb +6 -0
- data/app/mailers/campaign_mailer.rb +26 -0
- data/app/models/campaign.rb +17 -0
- data/app/models/email.rb +30 -0
- data/app/models/recipient.rb +28 -0
- data/app/views/emails/unsubscribe.html.haml +1 -0
- data/config/routes.rb +3 -0
- data/lib/generators/homing/homing_generator.rb +20 -0
- data/lib/generators/homing/templates/create_campaigns.rb +18 -0
- data/lib/generators/homing/templates/create_emails.rb +21 -0
- data/lib/generators/homing/templates/create_recipients.rb +15 -0
- data/lib/hasher.rb +29 -0
- data/lib/homing/engine.rb +14 -0
- data/lib/homing/railties/tasks.rake +51 -0
- data/lib/homing.rb +3 -0
- metadata +94 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
class CampaignMailer < ActionMailer::Base
|
2
|
+
default :from => Homing::Engine.config.default_email
|
3
|
+
|
4
|
+
def notification(recipient, campaign)
|
5
|
+
recipient.prepare!
|
6
|
+
recipient_email = recipient.email
|
7
|
+
@edition = campaign.name
|
8
|
+
if recipient_email.name.present?
|
9
|
+
to = "#{recipient_email.name} <#{recipient_email.address}>"
|
10
|
+
@text_message = campaign.text_message.gsub(/RECIPIENT_NAME/,recipient_email.name)
|
11
|
+
@html_message = campaign.html_message.gsub(/RECIPIENT_NAME/,recipient_email.name)
|
12
|
+
else
|
13
|
+
to = recipient_email.address
|
14
|
+
@text_message = campaign.text_message.gsub(/RECIPIENT_NAME/,campaign.default_name)
|
15
|
+
@html_message = campaign.html_message.gsub(/RECIPIENT_NAME/,campaign.default_name)
|
16
|
+
end
|
17
|
+
subject, @text_message, @html_message = [campaign.subject, @text_message, @html_message].map {|text|
|
18
|
+
text.gsub(/BUSINESS_NAME/,recipient_email.business)}.map {|text|
|
19
|
+
text.gsub(/BUSINESS_URL/,recipient_email.business_url)}.map {|text|
|
20
|
+
text.gsub(/ASSET_HOST/,Homing::Engine.config.asset_host)}
|
21
|
+
@html_message = @html_message.html_safe
|
22
|
+
@unsubscribe_token = recipient_email.unsubscribe_token
|
23
|
+
|
24
|
+
recipient.sending! if mail(:to => to, :subject => subject)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Campaign < ActiveRecord::Base
|
2
|
+
has_many :recipients
|
3
|
+
attr_accessor :recipients_list
|
4
|
+
after_create :save_recipients
|
5
|
+
validates_presence_of :name, :subject
|
6
|
+
|
7
|
+
def save_recipients
|
8
|
+
recipients_list.split("\n").each do |row|
|
9
|
+
columns = row.split("|")
|
10
|
+
email = Email.find_by_address(columns[1]) || Email.create!(:name => columns[0],
|
11
|
+
:address => columns[1], :business => columns[2],
|
12
|
+
:owner_id => columns[3], :owner_type => columns[4],
|
13
|
+
:business_url => columns[5])
|
14
|
+
self.recipients.create(:email => email) unless email.unsubscribed?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/app/models/email.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'hasher'
|
2
|
+
|
3
|
+
class Email < ActiveRecord::Base
|
4
|
+
include Hasher
|
5
|
+
include ActiveRecord::Transitions
|
6
|
+
|
7
|
+
validates_presence_of :address
|
8
|
+
validates_uniqueness_of :address
|
9
|
+
validates_presence_of :unsubscribe_token
|
10
|
+
validates_uniqueness_of :unsubscribe_token
|
11
|
+
before_validation :set_unsubscribe_token
|
12
|
+
belongs_to :owner, :polymorphic => true
|
13
|
+
|
14
|
+
state_machine do
|
15
|
+
state :active
|
16
|
+
state :unsubscribed
|
17
|
+
|
18
|
+
event :unsubscribe do
|
19
|
+
transitions :to => :unsubscribed, :from => :active, :on_transition => :mark_unsubscribed_at
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
def set_unsubscribe_token
|
25
|
+
self.unsubscribe_token = generate_hash(self.address)
|
26
|
+
end
|
27
|
+
def mark_unsubscribed_at(unsubscribed_at = Time.now)
|
28
|
+
self.unsubscribed_at = unsubscribed_at
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Recipient < ActiveRecord::Base
|
2
|
+
include ActiveRecord::Transitions
|
3
|
+
|
4
|
+
belongs_to :email
|
5
|
+
validates_uniqueness_of :email_id, :scope => :campaign_id
|
6
|
+
belongs_to :campaign
|
7
|
+
|
8
|
+
state_machine do
|
9
|
+
state :pending
|
10
|
+
state :ready
|
11
|
+
state :sent
|
12
|
+
|
13
|
+
event :prepare do
|
14
|
+
transitions :to => :ready, :from => :pending
|
15
|
+
end
|
16
|
+
event :sending do
|
17
|
+
transitions :to => :sent, :from => :ready
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
state_machine.states.collect(&:name).each do |name|
|
22
|
+
scope name, where(:state => name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def state_enum
|
26
|
+
Recipient.state_machine.states.collect(&:name).map {|name| name.to_s}
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
= @email.present? ? "Your email #{@email.address} was unsubscribed." : "No such email found."
|
data/config/routes.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
class HomingGenerator < Rails::Generators::Base
|
5
|
+
include Rails::Generators::Migration
|
6
|
+
|
7
|
+
def self.source_root
|
8
|
+
File.join(File.dirname(__FILE__), 'templates')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.next_migration_number(dirname)
|
12
|
+
current_migration_number(dirname) + 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_migration_file
|
16
|
+
migration_template "create_campaigns.rb", "db/migrate/create_campaigns.rb"
|
17
|
+
migration_template "create_emails.rb", "db/migrate/create_emails.rb"
|
18
|
+
migration_template "create_recipients.rb", "db/migrate/create_recipients.rb"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateCampaigns < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :campaigns do |t|
|
4
|
+
t.string :name
|
5
|
+
t.string :default_name
|
6
|
+
t.string :subject
|
7
|
+
t.text :text_message
|
8
|
+
t.text :html_message
|
9
|
+
t.boolean :complete
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :campaigns
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class CreateEmails < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :emails do |t|
|
4
|
+
t.string :name
|
5
|
+
t.string :address
|
6
|
+
t.string :business
|
7
|
+
t.integer :owner_id
|
8
|
+
t.string :owner_type
|
9
|
+
t.string :business_url
|
10
|
+
t.string :state
|
11
|
+
t.string :unsubscribe_token
|
12
|
+
t.datetime :unsubscribed_at
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :emails, :address
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :emails
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateRecipients < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :recipients do |t|
|
4
|
+
t.string :state
|
5
|
+
t.references :email
|
6
|
+
t.references :campaign
|
7
|
+
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.down
|
13
|
+
drop_table :recipients
|
14
|
+
end
|
15
|
+
end
|
data/lib/hasher.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hasher
|
2
|
+
require 'digest/sha1'
|
3
|
+
SECRET = "myhasheriscoolerthanyourhasher"
|
4
|
+
|
5
|
+
def generate_hash(string)
|
6
|
+
encrypt string
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
def encrypt(string)
|
11
|
+
Digest::SHA1.hexdigest(salt + string)
|
12
|
+
end
|
13
|
+
|
14
|
+
def salt
|
15
|
+
Digest::SHA1.hexdigest(SECRET + time_stamp + random)
|
16
|
+
end
|
17
|
+
|
18
|
+
def time_stamp(time = Time.now)
|
19
|
+
time.strftime("%d%Y%l%M").to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def random(size = 10)
|
23
|
+
(0...size).map { character_set[ rand(character_set.size) ] }.join
|
24
|
+
end
|
25
|
+
|
26
|
+
def character_set
|
27
|
+
[('0'..'9'),('A'..'Z')].map { |range| range.to_a }.flatten
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "homing"
|
2
|
+
require "rails"
|
3
|
+
require "transitions"
|
4
|
+
require "active_record/transitions"
|
5
|
+
|
6
|
+
module Homing
|
7
|
+
class Engine < Rails::Engine
|
8
|
+
config.default_email = "mail@example.com"
|
9
|
+
config.asset_host = "http://example.com"
|
10
|
+
rake_tasks do
|
11
|
+
load "homing/railties/tasks.rake"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
namespace :homing do
|
2
|
+
desc "Campaign notifier"
|
3
|
+
task :notify, [:campaign_id, :limit] => :environment do |t, args|
|
4
|
+
campaign_id = args[:campaign_id]
|
5
|
+
limit = (args[:limit] || 200).to_i
|
6
|
+
campaign = Campaign.find(campaign_id) rescue nil
|
7
|
+
if campaign.present? && campaign.complete.blank?
|
8
|
+
recipients = campaign.recipients.pending.limit(limit)
|
9
|
+
puts "Sending email to: #{recipients.count} addresses"
|
10
|
+
recipients.each_with_index do |recipient, index|
|
11
|
+
begin
|
12
|
+
puts "#{index}. #{recipient.email.address}"
|
13
|
+
CampaignMailer.notification(recipient, campaign).deliver unless recipient.email.unsubscribed?
|
14
|
+
rescue Exception => e
|
15
|
+
puts "ERROR: #{recipient.email.address}"
|
16
|
+
puts e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
campaign.update_attribute(:complete, true) if campaign.recipients.pending.count.zero? && campaign.recipients.ready.count.zero?
|
20
|
+
else
|
21
|
+
puts campaign.present? ? "Campaign complete!" : "Campaign not found!"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Campaign summary"
|
26
|
+
task :summary, [:campaign_id] => :environment do |t, args|
|
27
|
+
campaign_id = args[:campaign_id]
|
28
|
+
campaign = Campaign.find(campaign_id) rescue nil
|
29
|
+
if campaign.present?
|
30
|
+
puts "Total emails: #{campaign.recipients.count}"
|
31
|
+
puts "Emails sent: #{campaign.recipients.sent.count}"
|
32
|
+
puts "Emails ready: #{campaign.recipients.ready.count}"
|
33
|
+
puts "Emails pending: #{campaign.recipients.pending.count}"
|
34
|
+
puts "Campaign complete!" if campaign.complete
|
35
|
+
else
|
36
|
+
puts "Campaign not found!"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Campaign reset"
|
41
|
+
task :reset, [:campaign_id] => :environment do |t, args|
|
42
|
+
campaign_id = args[:campaign_id]
|
43
|
+
campaign = Campaign.find(campaign_id) rescue nil
|
44
|
+
if campaign.present?
|
45
|
+
campaign.update_attribute(:complete, false)
|
46
|
+
campaign.recipients.update_all("state = 'pending'")
|
47
|
+
else
|
48
|
+
puts "Campaign not found!"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/homing.rb
ADDED
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: homing
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Codrea
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-08-14 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
type: :runtime
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
name: transitions
|
33
|
+
version_requirements: *id001
|
34
|
+
prerelease: false
|
35
|
+
description:
|
36
|
+
email:
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- app/controllers/emails_controller.rb
|
45
|
+
- app/mailers/campaign_mailer.rb
|
46
|
+
- app/models/campaign.rb
|
47
|
+
- app/models/email.rb
|
48
|
+
- app/models/recipient.rb
|
49
|
+
- app/views/emails/unsubscribe.html.haml
|
50
|
+
- config/routes.rb
|
51
|
+
- lib/generators/homing/homing_generator.rb
|
52
|
+
- lib/generators/homing/templates/create_campaigns.rb
|
53
|
+
- lib/generators/homing/templates/create_emails.rb
|
54
|
+
- lib/generators/homing/templates/create_recipients.rb
|
55
|
+
- lib/hasher.rb
|
56
|
+
- lib/homing.rb
|
57
|
+
- lib/homing/engine.rb
|
58
|
+
- lib/homing/railties/tasks.rake
|
59
|
+
has_rdoc: true
|
60
|
+
homepage:
|
61
|
+
licenses: []
|
62
|
+
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.5.2
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Homing
|
93
|
+
test_files: []
|
94
|
+
|