smailer 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/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +25 -0
- data/Rakefile +2 -0
- data/lib/smailer.rb +5 -0
- data/lib/smailer/models.rb +10 -0
- data/lib/smailer/models/finished_mail.rb +41 -0
- data/lib/smailer/models/mail_campaign.rb +54 -0
- data/lib/smailer/models/mail_key.rb +42 -0
- data/lib/smailer/models/mailing_list.rb +13 -0
- data/lib/smailer/models/property.rb +54 -0
- data/lib/smailer/models/queued_mail.rb +41 -0
- data/lib/smailer/tasks.rb +5 -0
- data/lib/smailer/tasks/send.rb +59 -0
- data/lib/smailer/version.rb +3 -0
- data/smailer.gemspec +22 -0
- metadata +93 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Simple newsletter mailer for Rails
|
2
|
+
|
3
|
+
## Intro
|
4
|
+
|
5
|
+
This project is a simple mailer for newsletters, which implements simple queue processing, basic campaign management and has some unsubscribe support.
|
6
|
+
|
7
|
+
It is intended to be used within a Rails project.
|
8
|
+
|
9
|
+
## Install
|
10
|
+
|
11
|
+
For Rails 3 projects, update your Gemfile and add:
|
12
|
+
gem 'smailer'
|
13
|
+
Then run `bundle install` and you should be ready to go.
|
14
|
+
|
15
|
+
## Usage and documentation
|
16
|
+
|
17
|
+
TODO
|
18
|
+
|
19
|
+
## Contribution
|
20
|
+
|
21
|
+
Patches are always welcome. In case you find any issues with this code, please use the project's [Issues](http://github.com/mitio/smailer/issues) page on Github to report them. Feel free to contribute! :)
|
22
|
+
|
23
|
+
## License
|
24
|
+
|
25
|
+
Released under the MIT license.
|
data/Rakefile
ADDED
data/lib/smailer.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Smailer
|
2
|
+
module Models
|
3
|
+
autoload :MailingList, 'smailer/models/mailing_list'
|
4
|
+
autoload :MailCampaign, 'smailer/models/mail_campaign'
|
5
|
+
autoload :MailKey, 'smailer/models/mail_key'
|
6
|
+
autoload :QueuedMail, 'smailer/models/queued_mail'
|
7
|
+
autoload :FinishedMail, 'smailer/models/finished_mail'
|
8
|
+
autoload :Property, 'smailer/models/property'
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Smailer
|
2
|
+
module Models
|
3
|
+
class FinishedMail < ActiveRecord::Base
|
4
|
+
class Statuses
|
5
|
+
FAILED = 0
|
6
|
+
SENT = 1
|
7
|
+
end
|
8
|
+
|
9
|
+
belongs_to :mail_campaign
|
10
|
+
|
11
|
+
validates_presence_of :mail_campaign_id, :from, :to, :retries, :status
|
12
|
+
validates_numericality_of :mail_campaign_id, :retries, :status, :only_integer => true, :allow_nil => true
|
13
|
+
validates_length_of :from, :to, :subject, :last_error, :maximum => 255
|
14
|
+
|
15
|
+
delegate :mailing_list, :to => :mail_campaign, :allow_nil => true
|
16
|
+
|
17
|
+
def status_text
|
18
|
+
Statuses.constants.each do |constant_name|
|
19
|
+
return constant_name if Statuses.const_get(constant_name) == status
|
20
|
+
end
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.add(queued_mail, status = Statuses::SENT)
|
25
|
+
finished = self.new
|
26
|
+
|
27
|
+
[:mail_campaign_id, :from, :to, :subject, :body_html, :body_text, :retries, :last_retry_at, :last_error].each do |field|
|
28
|
+
finished.send("#{field}=", queued_mail.send(field))
|
29
|
+
end
|
30
|
+
|
31
|
+
finished.status = status
|
32
|
+
finished.sent_at = Time.now if status == Statuses::SENT
|
33
|
+
|
34
|
+
finished.save!
|
35
|
+
queued_mail.destroy
|
36
|
+
|
37
|
+
finished
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Smailer
|
2
|
+
module Models
|
3
|
+
class MailCampaign < ActiveRecord::Base
|
4
|
+
class UnsubscribeMethods
|
5
|
+
URL = 1
|
6
|
+
REPLY = 2
|
7
|
+
BOUNCED = 4
|
8
|
+
end
|
9
|
+
|
10
|
+
belongs_to :mailing_list
|
11
|
+
has_many :queued_mails, :dependent => :destroy
|
12
|
+
has_many :finished_mails
|
13
|
+
|
14
|
+
validates_presence_of :mailing_list_id, :from
|
15
|
+
validates_numericality_of :mailing_list_id, :unsubscribe_methods, :only_integer => true, :allow_nil => true
|
16
|
+
validates_length_of :from, :subject, :maximum => 255, :allow_nil => true
|
17
|
+
|
18
|
+
attr_accessible :mailing_list_id, :from, :subject, :body_html, :body_text
|
19
|
+
|
20
|
+
def add_unsubscribe_method(method)
|
21
|
+
self.unsubscribe_methods = (self.unsubscribe_methods || 0) | method
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_unsubscribe_method(method)
|
25
|
+
if has_unsubscribe_method?(method)
|
26
|
+
self.unsubscribe_methods = (self.unsubscribe_methods || 0) ^ method
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_unsubscribe_method?(method)
|
31
|
+
(unsubscribe_methods || 0) & method === method
|
32
|
+
end
|
33
|
+
|
34
|
+
def active_unsubscribe_methods
|
35
|
+
self.class.unsubscribe_methods.reject do |method, method_name|
|
36
|
+
not has_unsubscribe_method?(method)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def name
|
41
|
+
"Campaign ##{id} (#{mailing_list.name})"
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.unsubscribe_methods
|
45
|
+
methods = {}
|
46
|
+
UnsubscribeMethods.constants.map do |method_name|
|
47
|
+
methods[UnsubscribeMethods.const_get(method_name)] = method_name
|
48
|
+
end
|
49
|
+
|
50
|
+
methods
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Smailer
|
4
|
+
module Models
|
5
|
+
class MailKey < ActiveRecord::Base
|
6
|
+
validates_presence_of :email, :key
|
7
|
+
validates_uniqueness_of :email
|
8
|
+
validates_uniqueness_of :key
|
9
|
+
validates_length_of :email, :key, :maximum => 255
|
10
|
+
|
11
|
+
attr_accessible :email, :key
|
12
|
+
|
13
|
+
before_validation :set_key
|
14
|
+
before_save :set_key
|
15
|
+
|
16
|
+
def self.generate(email)
|
17
|
+
Digest::MD5.hexdigest("The #{email} and our great secret, which lies in Mt. Asgard!")
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get(email)
|
21
|
+
email = extract_email(email)
|
22
|
+
key = generate(email)
|
23
|
+
stored = where(:key => key).first
|
24
|
+
|
25
|
+
create :email => email, :key => key unless stored
|
26
|
+
|
27
|
+
key
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.extract_email(email)
|
31
|
+
email = $1.strip if email =~ /<(.+@.+)>\s*$/
|
32
|
+
email
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def set_key
|
38
|
+
self.key = self.class.generate(email)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Smailer
|
2
|
+
module Models
|
3
|
+
class MailingList < ActiveRecord::Base
|
4
|
+
has_many :mail_campaigns
|
5
|
+
|
6
|
+
validates_presence_of :name
|
7
|
+
validates_uniqueness_of :name
|
8
|
+
validates_length_of :name, :maximum => 255
|
9
|
+
|
10
|
+
attr_accessible :name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Smailer
|
2
|
+
module Models
|
3
|
+
class Property < ActiveRecord::Base
|
4
|
+
set_table_name 'smailer_properties'
|
5
|
+
|
6
|
+
validates_presence_of :name
|
7
|
+
validates_uniqueness_of :name
|
8
|
+
|
9
|
+
attr_accessible :name, :value, :notes
|
10
|
+
|
11
|
+
@@cache_created_at = Time.now
|
12
|
+
|
13
|
+
def self.clear_cache_if_needed!
|
14
|
+
if Time.now - @@cache_created_at > 1.minute
|
15
|
+
remove_class_variable :@@properties rescue nil
|
16
|
+
@@cache_created_at = Time.now
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.setup_cache!
|
21
|
+
self.clear_cache_if_needed!
|
22
|
+
@@properties ||= {} unless defined?(@@properties)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.get(name)
|
26
|
+
self.setup_cache!
|
27
|
+
name = name.to_s
|
28
|
+
|
29
|
+
unless @@properties.has_key?(name)
|
30
|
+
property = find_by_name name
|
31
|
+
@@properties[name] = property ? property.value : nil
|
32
|
+
end
|
33
|
+
|
34
|
+
@@properties[name]
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.get_all(name)
|
38
|
+
self.setup_cache!
|
39
|
+
name, cache_key = name.to_s, "#{name}[]"
|
40
|
+
|
41
|
+
unless @@properties.has_key?(cache_key)
|
42
|
+
properties = all(:conditions => ['name LIKE ?', "#{name}%"]).map { |p| [p.name, p.value] }
|
43
|
+
@@properties[cache_key] = Hash[ properties ]
|
44
|
+
end
|
45
|
+
|
46
|
+
@@properties[cache_key]
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.get_boolean(name)
|
50
|
+
self.get(name).to_s.strip =~ /^(true|t|yes|y|on|1)$/i ? true : false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Smailer
|
2
|
+
module Models
|
3
|
+
class QueuedMail < ActiveRecord::Base
|
4
|
+
belongs_to :mail_campaign
|
5
|
+
|
6
|
+
validates_presence_of :mail_campaign_id, :to
|
7
|
+
validates_uniqueness_of :to, :scope => :mail_campaign_id
|
8
|
+
validates_numericality_of :mail_campaign_id, :retries, :only_integer => true, :allow_nil => true
|
9
|
+
validates_length_of :to, :last_error, :maximum => 255
|
10
|
+
|
11
|
+
attr_accessible :mail_campaign_id, :to
|
12
|
+
|
13
|
+
delegate :from, :subject, :mailing_list, :to => :mail_campaign, :allow_nil => true
|
14
|
+
|
15
|
+
def body_html
|
16
|
+
interpolate mail_campaign.body_html
|
17
|
+
end
|
18
|
+
|
19
|
+
def body_text
|
20
|
+
interpolate mail_campaign.body_text
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def interpolate(text)
|
26
|
+
{
|
27
|
+
:email => to,
|
28
|
+
:escaped_email => lambda { ERB::Util.h(to) },
|
29
|
+
:email_key => lambda { MailKey.get(to) },
|
30
|
+
:mailing_list_id => lambda { mailing_list.id },
|
31
|
+
}.each do |variable, interpolation|
|
32
|
+
text.gsub! "%{#{variable}}" do
|
33
|
+
interpolation.respond_to?(:call) ? interpolation.call : interpolation.to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
text
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
module Smailer
|
4
|
+
module Tasks
|
5
|
+
class Send
|
6
|
+
def self.execute
|
7
|
+
Mail.defaults do
|
8
|
+
delivery_method Rails.configuration.action_mailer.delivery_method
|
9
|
+
end
|
10
|
+
|
11
|
+
batch_size = Smailer::Models::Property.get('queue.batch_size').to_i
|
12
|
+
max_retries = Smailer::Models::Property.get('queue.max_retries').to_i
|
13
|
+
max_lifetime = Smailer::Models::Property.get('queue.max_lifetime').to_i
|
14
|
+
|
15
|
+
results = []
|
16
|
+
Smailer::Models::QueuedMail.order(:retries.asc, :id.asc).limit(batch_size).each do |queue_item|
|
17
|
+
# try to send the email
|
18
|
+
mail = Mail.new do
|
19
|
+
from queue_item.from
|
20
|
+
to queue_item.to
|
21
|
+
subject queue_item.subject
|
22
|
+
|
23
|
+
text_part { body queue_item.body_text }
|
24
|
+
html_part { body queue_item.body_html; content_type 'text/html; charset=UTF-8' }
|
25
|
+
end
|
26
|
+
mail.raise_delivery_errors = true
|
27
|
+
|
28
|
+
queue_item.last_retry_at = Time.now
|
29
|
+
queue_item.retries += 1
|
30
|
+
|
31
|
+
begin
|
32
|
+
# commense delivery
|
33
|
+
mail.deliver
|
34
|
+
rescue Exception => e
|
35
|
+
# failed, we have.
|
36
|
+
queue_item.last_error = "#{e.class.name}: #{e.message}"
|
37
|
+
queue_item.save
|
38
|
+
|
39
|
+
# check if the message hasn't expired;
|
40
|
+
retries_exceeded = max_retries > 0 && queue_item.retries >= max_retries
|
41
|
+
too_old = max_lifetime > 0 && (Time.now - queue_item.created_at) >= max_lifetime
|
42
|
+
|
43
|
+
if retries_exceeded || too_old
|
44
|
+
# the message has expired; move to finished_mails
|
45
|
+
Smailer::Models::FinishedMail.add(queue_item, Smailer::Models::FinishedMail::Statuses::FAILED)
|
46
|
+
end
|
47
|
+
results.push [queue_item, :failed]
|
48
|
+
else
|
49
|
+
# great job, message sent
|
50
|
+
Smailer::Models::FinishedMail.add(queue_item, Smailer::Models::FinishedMail::Statuses::SENT)
|
51
|
+
results.push [queue_item, :sent]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
results
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/smailer.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/smailer/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "smailer"
|
6
|
+
s.version = Smailer::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ['Dimitar Dimitrov']
|
9
|
+
s.email = ['wireman@gmail.com']
|
10
|
+
s.homepage = "http://github.com/mitio/smailer"
|
11
|
+
s.summary = "A simple newsletter mailer for Rails."
|
12
|
+
s.description = "A simple mailer for newsletters with basic campaign management, queue and unsubscribe support. To be used within Rails."
|
13
|
+
|
14
|
+
s.required_rubygems_version = ">= 1.3.6"
|
15
|
+
s.rubyforge_project = "smailer"
|
16
|
+
|
17
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
21
|
+
s.require_path = 'lib'
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: smailer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Dimitar Dimitrov
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-09-02 00:00:00 +03:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: bundler
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 0
|
30
|
+
- 0
|
31
|
+
version: 1.0.0
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
description: A simple mailer for newsletters with basic campaign management, queue and unsubscribe support. To be used within Rails.
|
35
|
+
email:
|
36
|
+
- wireman@gmail.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- Gemfile
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- lib/smailer.rb
|
49
|
+
- lib/smailer/models.rb
|
50
|
+
- lib/smailer/models/finished_mail.rb
|
51
|
+
- lib/smailer/models/mail_campaign.rb
|
52
|
+
- lib/smailer/models/mail_key.rb
|
53
|
+
- lib/smailer/models/mailing_list.rb
|
54
|
+
- lib/smailer/models/property.rb
|
55
|
+
- lib/smailer/models/queued_mail.rb
|
56
|
+
- lib/smailer/tasks.rb
|
57
|
+
- lib/smailer/tasks/send.rb
|
58
|
+
- lib/smailer/version.rb
|
59
|
+
- smailer.gemspec
|
60
|
+
has_rdoc: true
|
61
|
+
homepage: http://github.com/mitio/smailer
|
62
|
+
licenses: []
|
63
|
+
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
segments:
|
81
|
+
- 1
|
82
|
+
- 3
|
83
|
+
- 6
|
84
|
+
version: 1.3.6
|
85
|
+
requirements: []
|
86
|
+
|
87
|
+
rubyforge_project: smailer
|
88
|
+
rubygems_version: 1.3.6
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: A simple newsletter mailer for Rails.
|
92
|
+
test_files: []
|
93
|
+
|