mail_manager 0.0.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.
- checksums.yaml +15 -0
- data/.DS_Store +0 -0
- data/.gitignore +20 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +27 -0
- data/Guardfile +24 -0
- data/LICENSE.txt +22 -0
- data/MIT-LICENSE +20 -0
- data/Manifest.txt +141 -0
- data/Procfile +4 -0
- data/README +243 -0
- data/README.md +29 -0
- data/README.rdoc +3 -0
- data/Rakefile +33 -0
- data/app/.DS_Store +0 -0
- data/app/assets/javascripts/mail_manager/application.js +15 -0
- data/app/assets/stylesheets/mail_manager/application.css +13 -0
- data/app/controllers/mail_manager/application_controller.rb +4 -0
- data/app/controllers/mail_manager/base_controller.rb +22 -0
- data/app/controllers/mail_manager/bounces_controller.rb +32 -0
- data/app/controllers/mail_manager/contacts_controller.rb +75 -0
- data/app/controllers/mail_manager/mailing_lists_controller.rb +49 -0
- data/app/controllers/mail_manager/mailings_controller.rb +102 -0
- data/app/controllers/mail_manager/messages_controller.rb +30 -0
- data/app/controllers/mail_manager/subscriptions_controller.rb +104 -0
- data/app/helpers/mail_manager/application_helper.rb +4 -0
- data/app/helpers/mail_manager/subscriptions_helper.rb +8 -0
- data/app/models/.DS_Store +0 -0
- data/app/models/mail_manager.rb +12 -0
- data/app/models/mail_manager/bounce.rb +133 -0
- data/app/models/mail_manager/contact.rb +91 -0
- data/app/models/mail_manager/contactable_registry.rb +190 -0
- data/app/models/mail_manager/mailable_registry.rb +127 -0
- data/app/models/mail_manager/mailer.rb +267 -0
- data/app/models/mail_manager/mailing.rb +266 -0
- data/app/models/mail_manager/mailing_list.rb +36 -0
- data/app/models/mail_manager/message.rb +127 -0
- data/app/models/mail_manager/subscription.rb +126 -0
- data/app/models/mail_manager/test_message.rb +175 -0
- data/app/models/status_history.rb +60 -0
- data/app/views/layouts/mail_manager/application.html.erb +14 -0
- data/app/views/mail_manager/bounces/_email_parts.html.erb +30 -0
- data/app/views/mail_manager/bounces/index.html.erb +32 -0
- data/app/views/mail_manager/bounces/show.html.erb +38 -0
- data/app/views/mail_manager/contacts/_form.html.erb +27 -0
- data/app/views/mail_manager/contacts/double_opt_in.html.erb +1 -0
- data/app/views/mail_manager/contacts/edit.html.erb +12 -0
- data/app/views/mail_manager/contacts/index.html.erb +86 -0
- data/app/views/mail_manager/contacts/new.html.erb +9 -0
- data/app/views/mail_manager/contacts/show.html.erb +22 -0
- data/app/views/mail_manager/contacts/subscribe.html.erb +2 -0
- data/app/views/mail_manager/contacts/thank_you.html.erb +12 -0
- data/app/views/mail_manager/help/_available_email_substitutions.html.erb +5 -0
- data/app/views/mail_manager/mailer/double_opt_in.erb +6 -0
- data/app/views/mail_manager/mailer/unsubscribed.erb +5 -0
- data/app/views/mail_manager/mailer/unsubscribed.html.erb +5 -0
- data/app/views/mail_manager/mailing_lists/_form.html.erb +20 -0
- data/app/views/mail_manager/mailing_lists/edit.html.erb +13 -0
- data/app/views/mail_manager/mailing_lists/index.html.erb +39 -0
- data/app/views/mail_manager/mailing_lists/new.html.erb +9 -0
- data/app/views/mail_manager/mailing_lists/show.html.erb +13 -0
- data/app/views/mail_manager/mailings/_form.html.erb +81 -0
- data/app/views/mail_manager/mailings/edit.html.erb +12 -0
- data/app/views/mail_manager/mailings/index.html.erb +52 -0
- data/app/views/mail_manager/mailings/new.html.erb +9 -0
- data/app/views/mail_manager/mailings/show.html.erb +28 -0
- data/app/views/mail_manager/mailings/test.html.erb +12 -0
- data/app/views/mail_manager/messages/index.html.erb +37 -0
- data/app/views/mail_manager/subscriptions/_form.html.erb +37 -0
- data/app/views/mail_manager/subscriptions/_subscriptions.html.erb +13 -0
- data/app/views/mail_manager/subscriptions/edit.html.erb +11 -0
- data/app/views/mail_manager/subscriptions/index.html.erb +32 -0
- data/app/views/mail_manager/subscriptions/new.html.erb +9 -0
- data/app/views/mail_manager/subscriptions/show.html.erb +8 -0
- data/app/views/mail_manager/subscriptions/unsubscribe.html.erb +2 -0
- data/app/views/mail_manager/subscriptions/unsubscribe_by_email_address.html.erb +13 -0
- data/config/daemons.yml +5 -0
- data/config/routes.rb +43 -0
- data/db/migrate/001_mail_mgr_initial.rb +84 -0
- data/db/migrate/002_mail_mgr_create_contact.rb +60 -0
- data/db/migrate/003_mail_mgr_test_message.rb +23 -0
- data/db/migrate/004_add_deleted_at_to_mailing_lists.rb +15 -0
- data/db/migrate/005_contacts_deleted_at.rb +15 -0
- data/db/migrate/006_mail_mgr_mailing_list_add_defaults_to_active.rb +15 -0
- data/db/migrate/007_mail_mgr_message_add_from_email_address.rb +15 -0
- data/db/mlm_migrate/001_mlm_initial.rb +67 -0
- data/db/mlm_migrate/002_mailable_as_polymorphic.rb +27 -0
- data/db/mlm_migrate/003_contact_as_polymorphic.rb +64 -0
- data/db/mlm_migrate/004_bounce_mlm_mailing_id.rb +26 -0
- data/db/mlm_migrate/005_mlm_to_mail_mgr_scoped.rb +29 -0
- data/engine_plan.rb +13 -0
- data/features/bounce_management.feature +0 -0
- data/features/contact_management.feature +24 -0
- data/features/mailable.feature +23 -0
- data/features/mailing_management.feature +78 -0
- data/features/message.feature +11 -0
- data/features/step_definitions/email_steps.rb +50 -0
- data/features/step_definitions/mlm_steps.rb +11 -0
- data/features/step_definitions/pickle_steps.rb +41 -0
- data/features/step_definitions/webrat_steps.rb +115 -0
- data/features/subscription_management.feature +17 -0
- data/features/support/env.rb +31 -0
- data/features/support/paths.rb +44 -0
- data/lib/daemons/mail_manager.rb +38 -0
- data/lib/daemons/mail_manager_ctl +15 -0
- data/lib/deleteable.rb +50 -0
- data/lib/lock.rb +36 -0
- data/lib/mail_manager.rb +5 -0
- data/lib/mail_manager/config.rb +50 -0
- data/lib/mail_manager/engine.rb +43 -0
- data/lib/mail_manager/version.rb +3 -0
- data/lib/tasks/mail_manager.rake +143 -0
- data/lib/tasks/mail_manager_tasks.rake +4 -0
- data/lib/tasks/rspec.rake +165 -0
- data/lib/workers/mail_manager/bounce_job.rb +52 -0
- data/lib/workers/mail_manager/mailing_job.rb +30 -0
- data/lib/workers/mail_manager/message_job.rb +38 -0
- data/lib/workers/mail_manager/test_message_job.rb +37 -0
- data/mail_manager.gemspec +29 -0
- data/script/rails +8 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/test_app/README.rdoc +261 -0
- data/spec/test_app/Rakefile +7 -0
- data/spec/test_app/app/assets/javascripts/application.js +15 -0
- data/spec/test_app/app/assets/javascripts/users.js +2 -0
- data/spec/test_app/app/assets/stylesheets/application.css +13 -0
- data/spec/test_app/app/assets/stylesheets/scaffold.css +56 -0
- data/spec/test_app/app/assets/stylesheets/users.css +4 -0
- data/spec/test_app/app/controllers/application_controller.rb +3 -0
- data/spec/test_app/app/controllers/users_controller.rb +83 -0
- data/spec/test_app/app/helpers/application_helper.rb +2 -0
- data/spec/test_app/app/helpers/users_helper.rb +2 -0
- data/spec/test_app/app/models/user.rb +13 -0
- data/spec/test_app/app/views/layouts/application.html.erb +14 -0
- data/spec/test_app/app/views/users/_form.html.erb +33 -0
- data/spec/test_app/app/views/users/edit.html.erb +6 -0
- data/spec/test_app/app/views/users/index.html.erb +29 -0
- data/spec/test_app/app/views/users/new.html.erb +5 -0
- data/spec/test_app/app/views/users/show.html.erb +25 -0
- data/spec/test_app/config.ru +4 -0
- data/spec/test_app/config/application.rb +65 -0
- data/spec/test_app/config/boot.rb +10 -0
- data/spec/test_app/config/database.yml +25 -0
- data/spec/test_app/config/environment.rb +14 -0
- data/spec/test_app/config/environments/development.rb +37 -0
- data/spec/test_app/config/environments/production.rb +67 -0
- data/spec/test_app/config/environments/test.rb +37 -0
- data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/test_app/config/initializers/inflections.rb +15 -0
- data/spec/test_app/config/initializers/mime_types.rb +5 -0
- data/spec/test_app/config/initializers/secret_token.rb +7 -0
- data/spec/test_app/config/initializers/session_store.rb +8 -0
- data/spec/test_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/test_app/config/locales/en.yml +5 -0
- data/spec/test_app/config/lockable.yml +3 -0
- data/spec/test_app/config/mail_manager.yml +21 -0
- data/spec/test_app/config/routes.rb +7 -0
- data/spec/test_app/db/migrate/20131217101010_create_users.rb +13 -0
- data/spec/test_app/db/migrate/20131221064151_mail_mgr_initial.rb +84 -0
- data/spec/test_app/db/migrate/20131221064152_mail_mgr_create_contact.rb +60 -0
- data/spec/test_app/db/migrate/20131221064153_mail_mgr_test_message.rb +23 -0
- data/spec/test_app/db/migrate/20131221064154_add_deleted_at_to_mailing_lists.rb +15 -0
- data/spec/test_app/db/migrate/20131221064155_contacts_deleted_at.rb +15 -0
- data/spec/test_app/db/migrate/20131221064156_mail_mgr_mailing_list_add_defaults_to_active.rb +15 -0
- data/spec/test_app/db/migrate/20131221064157_mail_mgr_message_add_from_email_address.rb +15 -0
- data/spec/test_app/db/migrate/20131221072600_create_delayed_jobs.rb +22 -0
- data/spec/test_app/db/schema.rb +131 -0
- data/spec/test_app/db/structure.sql +31 -0
- data/spec/test_app/public/404.html +26 -0
- data/spec/test_app/public/422.html +26 -0
- data/spec/test_app/public/500.html +25 -0
- data/spec/test_app/public/favicon.ico +0 -0
- data/spec/test_app/script/delayed_job +5 -0
- data/spec/test_app/script/lockable +36 -0
- data/spec/test_app/script/rails +6 -0
- data/spec/test_app/spec/controllers/users_controller_spec.rb +160 -0
- data/spec/test_app/spec/factories/mailing_lists.rb +7 -0
- data/spec/test_app/spec/factories/mailings.rb +6 -0
- data/spec/test_app/spec/factories/original_factories.rb.txt +107 -0
- data/spec/test_app/spec/factories/users.rb +10 -0
- data/spec/test_app/spec/models/mail_manager/bounce_spec.rb +17 -0
- data/spec/test_app/spec/models/mail_manager/mailing_list_spec.rb +15 -0
- data/spec/test_app/spec/models/user_spec.rb +37 -0
- data/spec/test_app/spec/requests/users_spec.rb +11 -0
- data/spec/test_app/spec/routing/users_routing_spec.rb +35 -0
- data/spec/test_app/spec/support/database_cleaner.rb +23 -0
- data/spec/test_app/spec/support/files/bad_utf8_chars.eml +32 -0
- data/spec/test_app/spec/views/users/edit.html.erb_spec.rb +24 -0
- data/spec/test_app/spec/views/users/index.html.erb_spec.rb +29 -0
- data/spec/test_app/spec/views/users/new.html.erb_spec.rb +24 -0
- data/spec/test_app/spec/views/users/show.html.erb_spec.rb +21 -0
- data/zeus.json +22 -0
- metadata +424 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
=begin rdoc
|
|
2
|
+
Author:: Chris Hauboldt (mailto:biz@lnstar.com)
|
|
3
|
+
Copyright:: 2009 Lone Star Internet Inc.
|
|
4
|
+
|
|
5
|
+
This class supplies the method for things to be "mailable" from the mailing list manager. Mailable things
|
|
6
|
+
need to be able to be found, have a distinguishable name, have "parts" which contain content for the email and
|
|
7
|
+
register themselves in initialization code. Below is a sample model class and its initialization code.
|
|
8
|
+
|
|
9
|
+
NOTE!!! you should order the parts in the order you would like them to be placed in the email... the last one seems to be the prefered type of many email clients, although some will have preference settings. In general put the one you want them to see last.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Mailable < ActiveRecord::Base
|
|
14
|
+
named_scope :active, :conditions => ['deleted_at is null and published_at<?',Time.now.utc]
|
|
15
|
+
def name
|
|
16
|
+
name + created_at
|
|
17
|
+
end
|
|
18
|
+
def email_html
|
|
19
|
+
"<html><body>Hello World!</body></html>"
|
|
20
|
+
end
|
|
21
|
+
def email_text
|
|
22
|
+
"Hello World!"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
require 'mailable'
|
|
28
|
+
MailableRegistry.register(MyMailable,{
|
|
29
|
+
:find_mailables => :active,
|
|
30
|
+
:name => :name,
|
|
31
|
+
:parts => [
|
|
32
|
+
['text/plain' => :email_text],
|
|
33
|
+
['text/html' => :email_html]
|
|
34
|
+
]
|
|
35
|
+
})
|
|
36
|
+
Rails.logger.warn "Registered Newsletter Mailable"
|
|
37
|
+
rescue => e
|
|
38
|
+
Rails.logger.warn "Couldn't register Newsletter Mailable #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
--
|
|
41
|
+
=end
|
|
42
|
+
|
|
43
|
+
module MailManager
|
|
44
|
+
class MailableRegistry
|
|
45
|
+
attr_reader :mailable_things
|
|
46
|
+
|
|
47
|
+
=begin rdoc
|
|
48
|
+
Registers a class as a "mailable" item.
|
|
49
|
+
Parameters::
|
|
50
|
+
klass => Class constant to be registered
|
|
51
|
+
methods => a hash which maps :find, :name, and mime type to methods; :parts {'mime-type' => :method}
|
|
52
|
+
|
|
53
|
+
Example Useage:
|
|
54
|
+
you may want to wrap your register in a rescue block if you don't know whether or not the
|
|
55
|
+
mailing list manager exists in your current project.
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
require 'mail_manager/mailable_registry'
|
|
59
|
+
MailableRegistry.register(MyMailable,{
|
|
60
|
+
:find_mailables => :active,
|
|
61
|
+
:name => :name,
|
|
62
|
+
:parts => [
|
|
63
|
+
['text/plain' => :email_text],
|
|
64
|
+
['text/html' => :email_html]
|
|
65
|
+
]
|
|
66
|
+
})
|
|
67
|
+
Rails.logger.debug "Registered Newsletter Mailable"
|
|
68
|
+
rescue => e
|
|
69
|
+
Rails.logger.debug "Couldn't register Newsletter Mailable #{e.message}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
=end
|
|
73
|
+
def self.register(klass,methods={})
|
|
74
|
+
Rails.logger.warn "Registered Mailable: #{klass.inspect} - #{methods.inspect}"
|
|
75
|
+
@@mailable_things.merge!({klass => methods})
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
=begin rdoc
|
|
79
|
+
Finds available mailable items by searching through all registered mailables, calling their finders and sorting by name.
|
|
80
|
+
=end
|
|
81
|
+
def self.find
|
|
82
|
+
mailable_items = []
|
|
83
|
+
@@mailable_things.each_pair do |thing,methods|
|
|
84
|
+
Rails.logger.debug "Gathering #{thing} mailables with #{methods[:find_mailables]}"
|
|
85
|
+
mailable_items += thing.constantize.send(methods[:find_mailables])
|
|
86
|
+
end
|
|
87
|
+
mailable_items.sort{|a,b| a.name <=> b.name}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
protected
|
|
92
|
+
# -- holds registrations of mailables
|
|
93
|
+
@@mailable_things = Hash.new
|
|
94
|
+
|
|
95
|
+
def self.mailable_things
|
|
96
|
+
@@mailable_things
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
module Mailable
|
|
100
|
+
def mailable_initialize_parts
|
|
101
|
+
@mailable_parts = []
|
|
102
|
+
MailableRegistry.mailable_things[self.class.name][:parts].each{|part,method|
|
|
103
|
+
@mailable_parts << [part, send(method)]
|
|
104
|
+
}
|
|
105
|
+
@mailable_parts
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def mailable_value(method)
|
|
109
|
+
return send(method) unless MailableRegistry.mailable_things[self.class.name] and
|
|
110
|
+
MailableRegistry.mailable_things[self.class.name][method]
|
|
111
|
+
send(MailableRegistry.mailable_things[self.class.name][method])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def mailable_parts
|
|
115
|
+
return @mailable_parts unless @mailable_parts.nil?
|
|
116
|
+
mailable_initialize_parts
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.included(base)
|
|
120
|
+
base.class_eval do
|
|
121
|
+
has_many :mailings, :as => :mailable, :class_name => "MailManager::Mailing"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
=begin rdoc
|
|
2
|
+
Author:: Chris Hauboldt (mailto:biz@lnstar.com)
|
|
3
|
+
Copyright:: 2009 Lone Star Internet Inc.
|
|
4
|
+
|
|
5
|
+
This class is responsible for actually sending email messages... its mostly just an ActionMailer, but also has the added functionality to send messages with inline images.
|
|
6
|
+
|
|
7
|
+
Messages:
|
|
8
|
+
unsubscribed - sends an email to notify the user that they have been removed
|
|
9
|
+
message - sends and Message
|
|
10
|
+
test_mailing - sends a test message for a mailing
|
|
11
|
+
mail - knows how to send any message based on the different "mime" parts its given
|
|
12
|
+
|
|
13
|
+
=end
|
|
14
|
+
|
|
15
|
+
require 'net/http'
|
|
16
|
+
require 'uri'
|
|
17
|
+
require "base64"
|
|
18
|
+
begin
|
|
19
|
+
require "mini_magick"
|
|
20
|
+
rescue => e
|
|
21
|
+
require 'rmagick' rescue nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
module MailManager
|
|
26
|
+
class Mailer < ActionMailer::Base
|
|
27
|
+
def unsubscribed(message,subscriptions)
|
|
28
|
+
@contact = message.contact
|
|
29
|
+
@recipients = @contact.email_address
|
|
30
|
+
@from = message.from_email_address
|
|
31
|
+
@message = message
|
|
32
|
+
@mailing_lists = subscriptions.reject{|subscription| subscription.mailing_list.nil?}.
|
|
33
|
+
collect{|subscription| subscription.mailing_list.name}
|
|
34
|
+
@subject = "Unsubscribed from #{@mailing_lists.join(',')} at #{MailManager.site_url}"
|
|
35
|
+
Rails.logger.debug "Really Sending Unsubscribed from #{@mailing_lists.first} to #{@contact.email_address}"
|
|
36
|
+
mail(to: @recipients, from: @from, subject: @subject)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# we do special junk ... so lets make them class methods
|
|
40
|
+
class << self
|
|
41
|
+
def deliver_message(message)
|
|
42
|
+
self.send_mail(message.subject,message.email_address_with_name,message.from_email_address,
|
|
43
|
+
message.parts,message.guid,message.mailing.include_images?)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def multipart_with_inline_images(subject,to_email_address,from_email_address,the_parts,message_id=nil,include_images=true)
|
|
47
|
+
text_source = the_parts.first[1];nil
|
|
48
|
+
original_html_source = the_parts.last[1];nil
|
|
49
|
+
mail = Mail.new do
|
|
50
|
+
to to_email_address
|
|
51
|
+
from from_email_address
|
|
52
|
+
subject subject
|
|
53
|
+
part :content_type => "multipart/alternative", :content_disposition => "inline" do |main|
|
|
54
|
+
main.part :content_type => "text/plain", :body => text_source
|
|
55
|
+
if include_images
|
|
56
|
+
main.part :content_type => "multipart/related" do |related|
|
|
57
|
+
(html_source,images) = MailManager::Mailer::inline_html_with_images(original_html_source)
|
|
58
|
+
images.each_with_index do |image,index|
|
|
59
|
+
related.attachments.inline[image[:filename]] = {
|
|
60
|
+
:content_id => image[:cid],
|
|
61
|
+
:content => image[:content]
|
|
62
|
+
}
|
|
63
|
+
html_source.gsub!(image[:cid],related.attachments[index].cid)
|
|
64
|
+
end
|
|
65
|
+
related.part :content_type => "text/html; charset=UTF-8", :body => html_source
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
main.part :content_type => "text/html; charset=UTF-8", :body => original_html_source
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
mail
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def multipart_alternative_without_images(subject,to_email_address,from_email_address,the_parts,message_id=nil,include_images=true)
|
|
76
|
+
text_source = the_parts.first[1];nil
|
|
77
|
+
original_html_source = the_parts.last[1];nil
|
|
78
|
+
mail = Mail.new do
|
|
79
|
+
to to_email_address
|
|
80
|
+
from from_email_address
|
|
81
|
+
subject subject
|
|
82
|
+
|
|
83
|
+
text_part do
|
|
84
|
+
body text_source
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
html_part do
|
|
88
|
+
content_type 'text/html; charset=UTF-8'
|
|
89
|
+
body original_html_source
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
mail
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def send_mail(subject,to_email_address,from_email_address,the_parts,message_id=nil,include_images=true)
|
|
96
|
+
include_images = (include_images and !MailManager.dont_include_images_domains.detect{|domain|
|
|
97
|
+
to_email_address.strip =~ /#{domain}>?$/})
|
|
98
|
+
mail = if include_images
|
|
99
|
+
multipart_with_inline_images(subject,to_email_address,from_email_address,the_parts,message_id,include_images)
|
|
100
|
+
else
|
|
101
|
+
multipart_alternative_without_images(subject,to_email_address,from_email_address,the_parts,message_id,include_images)
|
|
102
|
+
end
|
|
103
|
+
mail.header['Return-Path'] = MailManager.bounce['email_address']
|
|
104
|
+
mail.header['X-Bounce-Guid'] = message_id if message_id
|
|
105
|
+
set_mail_settings(mail)
|
|
106
|
+
mail.deliver!
|
|
107
|
+
Rails.logger.info "Sent mail to: #{to_email_address}"
|
|
108
|
+
Rails.logger.debug mail.to_s
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def set_mail_settings(mail)
|
|
112
|
+
mail.delivery_method ActionMailer::Base.delivery_method.eql?(:letter_opener) ? :test : ActionMailer::Base.delivery_method
|
|
113
|
+
# letter opener blows up!
|
|
114
|
+
# Ex set options!
|
|
115
|
+
# mail.delivery_method.settings.merge!( {
|
|
116
|
+
# user_name: 'bobo',
|
|
117
|
+
# password: 'Secret1!',
|
|
118
|
+
# address: 'mail.lnstar.com',
|
|
119
|
+
# domain: 'mail.lnstar.com',
|
|
120
|
+
# enable_starttls_auto: true,
|
|
121
|
+
# authentication: :plain,
|
|
122
|
+
# port: 587
|
|
123
|
+
# } )
|
|
124
|
+
|
|
125
|
+
mail.delivery_method.settings.merge!(
|
|
126
|
+
(case method
|
|
127
|
+
when :smtp then ActionMailer::Base.smtp_settings
|
|
128
|
+
when :sendmail then ActionMailer::Base.sendmail_settings
|
|
129
|
+
else
|
|
130
|
+
{}
|
|
131
|
+
end rescue {})
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def inline_attachment(params, &block)
|
|
136
|
+
params = { :content_type => params } if String === params
|
|
137
|
+
params = { :disposition => "inline",
|
|
138
|
+
:transfer_encoding => "base64" }.merge(params)
|
|
139
|
+
params[:headers] ||= {}
|
|
140
|
+
params[:headers]['Content-ID'] = params[:cid]
|
|
141
|
+
params
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def image_mime_types(extension)
|
|
145
|
+
case extension.downcase
|
|
146
|
+
when 'bmp' then 'image/bmp'
|
|
147
|
+
when 'cod' then 'image/cis-cod'
|
|
148
|
+
when 'gif' then 'image/gif'
|
|
149
|
+
when 'ief' then 'image/ief'
|
|
150
|
+
when 'jpe' then 'image/jpeg'
|
|
151
|
+
when 'jpeg' then 'image/jpeg'
|
|
152
|
+
when 'jpg' then 'image/jpeg'
|
|
153
|
+
when 'png' then 'image/png'
|
|
154
|
+
when 'jfif' then 'image/pipeg'
|
|
155
|
+
when 'svg' then 'image/svg+xml'
|
|
156
|
+
when 'tif' then 'image/tiff'
|
|
157
|
+
when 'tiff' then 'image/tiff'
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def get_extension_from_data(image_data)
|
|
162
|
+
if defined?(MiniMagick)
|
|
163
|
+
MiniMagick::Image.read(image_data)[:format] || ''
|
|
164
|
+
elsif defined?(Magick)
|
|
165
|
+
Magick::Image.from_blob(image_data).first.format || ''
|
|
166
|
+
else
|
|
167
|
+
''
|
|
168
|
+
end
|
|
169
|
+
rescue => e
|
|
170
|
+
''
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def inline_html_with_images(html_source)
|
|
174
|
+
parsed_data = html_source.split(/(<\s*img[^>]+src\s*=\s*["'])([^"']*)(["'])/i)
|
|
175
|
+
images = Array.new
|
|
176
|
+
final_html = ''
|
|
177
|
+
image_errors = ''
|
|
178
|
+
parsed_data.each_with_index do |data,index|
|
|
179
|
+
if(index % 4 == 2)
|
|
180
|
+
image = Hash.new()
|
|
181
|
+
image[:cid] = Base64.encode64(data).gsub(/\s*/,'').reverse[0..59]
|
|
182
|
+
final_html << "cid:#{image[:cid]}"
|
|
183
|
+
#only attach new images!
|
|
184
|
+
next if images.detect{|this_image| this_image[:cid].eql?(image[:cid])}
|
|
185
|
+
begin
|
|
186
|
+
image[:content] = fetch(data)
|
|
187
|
+
rescue => e
|
|
188
|
+
image_errors += "Couldn't fetch url '#{data}'<!--, #{e.message} - #{e.backtrace.join("\n")}-->\n"
|
|
189
|
+
end
|
|
190
|
+
image[:filename] = filename = File.basename(data)
|
|
191
|
+
extension = filename.gsub(/^.*\./,'').downcase
|
|
192
|
+
Rails.logger.debug "Fetching Image for: #{filename} #{image[:content].to_s[0..30]}"
|
|
193
|
+
extension = get_extension_from_data(image[:content]) if image_mime_types(extension).blank?
|
|
194
|
+
image_errors += "Couldn't find mime type for #{extension} on #{data}" if image_mime_types(extension).blank?
|
|
195
|
+
image[:content_type] = image_mime_types(extension)
|
|
196
|
+
images << image
|
|
197
|
+
else
|
|
198
|
+
final_html << data
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
raise image_errors unless image_errors.eql?('')
|
|
202
|
+
[final_html,images]
|
|
203
|
+
# related_part = Mail::Part.new do
|
|
204
|
+
# body final_html
|
|
205
|
+
# end
|
|
206
|
+
# images.each do |image|
|
|
207
|
+
# related_part.part inline_attachment(image)
|
|
208
|
+
# end
|
|
209
|
+
# related_part.content_type = 'multipart/related'
|
|
210
|
+
# related_part
|
|
211
|
+
|
|
212
|
+
# related_part = Mail::Part.new do
|
|
213
|
+
# content_type 'multipart/related'
|
|
214
|
+
# # content_type 'text/html; charset=UTF-8'
|
|
215
|
+
# # body final_html
|
|
216
|
+
# end
|
|
217
|
+
# related_part.parts << Mail::Part.new do
|
|
218
|
+
# content_type 'text/html; charset=UTF-8'
|
|
219
|
+
# body final_html
|
|
220
|
+
# end
|
|
221
|
+
# images.each do |image|
|
|
222
|
+
# related_part.attachments[image[:filename]] = image[:body]
|
|
223
|
+
# end
|
|
224
|
+
# related_part.content_type = 'multipart/related'
|
|
225
|
+
# related_part.parts.first.content_type = 'text/html; charset=UTF-8'
|
|
226
|
+
# related_part.parts.first.header['Content-Disposition'] = 'inline'
|
|
227
|
+
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def local_ips
|
|
231
|
+
`/sbin/ifconfig`
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def request_local?(uri_str)
|
|
235
|
+
uri = URI.parse(uri_str)
|
|
236
|
+
ip_address = `host #{uri.host}`.gsub(/.*has address ([\d\.]+)\s.*/m,"\\1")
|
|
237
|
+
local_ips.include?(ip_address)
|
|
238
|
+
rescue => e
|
|
239
|
+
false
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def fetch(uri_str, limit = 10)
|
|
243
|
+
# You should choose better exception.
|
|
244
|
+
# raise ArgumentError, 'HTTP redirect too deep' if limit == 0
|
|
245
|
+
|
|
246
|
+
# response = Net::HTTP.get_response(URI.parse(uri_str))
|
|
247
|
+
# case response
|
|
248
|
+
# when Net::HTTPSuccess then response.body
|
|
249
|
+
# when Net::HTTPRedirection then fetch(response['location'], limit - 1)
|
|
250
|
+
# else
|
|
251
|
+
# response.error!
|
|
252
|
+
# end
|
|
253
|
+
body = ''
|
|
254
|
+
Curl.get(uri_str) do |http|
|
|
255
|
+
http.follow_location = true
|
|
256
|
+
http.interface = '127.0.0.1' if request_local?(uri_str)
|
|
257
|
+
http.on_success{|response| body = response.body}
|
|
258
|
+
end
|
|
259
|
+
raise Exception.new("Couldn't fetch URL: #{uri_str}") unless body.present?
|
|
260
|
+
body
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
=begin rdoc
|
|
3
|
+
Author:: Chris Hauboldt (mailto:biz@lnstar.com)
|
|
4
|
+
Copyright:: 2009 Lone Star Internet Inc.
|
|
5
|
+
|
|
6
|
+
Mailing is used to send a Mailable object to an MailingList. It is 'ready' to send when its status is 'scheduled' and 'shcheduled_at' is in the past. MailingJob will poll for 'ready' Mailings and send them.
|
|
7
|
+
|
|
8
|
+
Statuses
|
|
9
|
+
pending - initial state - mailing is waiting to be tested and scheduled - will not send
|
|
10
|
+
scheduled - mailing will be sent when it is 'ready' (its status is 'scheduled' and 'shcheduled_at' is in the past)
|
|
11
|
+
processing - MailingJob is sending the messages for the mailing
|
|
12
|
+
completed - Mailing has been sent
|
|
13
|
+
|
|
14
|
+
=end
|
|
15
|
+
|
|
16
|
+
module MailManager
|
|
17
|
+
class Mailing < ActiveRecord::Base
|
|
18
|
+
self.table_name = "#{MailManager.table_prefix}mailings"
|
|
19
|
+
has_many :messages, :class_name => 'MailManager::Message'
|
|
20
|
+
has_many :test_messages, :class_name => 'MailManager::TestMessage'
|
|
21
|
+
has_many :bounces, :class_name => 'MailManager::Bounce'
|
|
22
|
+
has_and_belongs_to_many :mailing_lists, :class_name => 'MailManager::MailingList',
|
|
23
|
+
:join_table => "#{MailManager.table_prefix}mailing_lists_#{MailManager.table_prefix}mailings"
|
|
24
|
+
#FIXME why does this break?
|
|
25
|
+
belongs_to :mailable, :polymorphic => true
|
|
26
|
+
|
|
27
|
+
accepts_nested_attributes_for :mailable
|
|
28
|
+
|
|
29
|
+
attr_accessor :bounce_count
|
|
30
|
+
|
|
31
|
+
validates_presence_of :subject
|
|
32
|
+
#validates_presence_of :mailable
|
|
33
|
+
|
|
34
|
+
scope :ready, lambda {{:conditions => ["(status='scheduled' AND scheduled_at < ?) OR status='resumed'",Time.now.utc]}}
|
|
35
|
+
scope :by_statuses, lambda {|*statuses| {:conditions => ["status in (#{statuses.collect{|bindings,status| '?'}.join(",")})",statuses].flatten}}
|
|
36
|
+
|
|
37
|
+
def self.with_bounces(bounce_status=nil)
|
|
38
|
+
bounce_status_condition = bounce_status.present? ? ActiveRecord::Base.send(:sanitize_sql_array,[" WHERE status=?", bounce_status]) : ''
|
|
39
|
+
bounce_query = "SELECT mailing_id, COUNT(id) AS count from #{MailManager.table_prefix}bounces #{bounce_status_condition} group by mailing_id"
|
|
40
|
+
bounce_data = Bounce.connection.execute(bounce_query).inject({}){|hash,(mailing_id,count)| hash.merge(mailing_id => count)}
|
|
41
|
+
mailings = scoped
|
|
42
|
+
mailings = mailings.where("id in (#{bounce_data.keys.select(&:present?).join(',')})") if bounce_data.keys.select(&:present?).present?
|
|
43
|
+
mailings.order("created_at desc").map{|mailing| mailing.bounce_count = bounce_data[mailing.id]; mailing}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
include StatusHistory
|
|
47
|
+
override_statuses(['pending','scheduled','processing','paused','resumed','cancelled','completed'],'pending')
|
|
48
|
+
before_create :set_default_status
|
|
49
|
+
|
|
50
|
+
attr_protected :id
|
|
51
|
+
|
|
52
|
+
def send_one_off_message(contact)
|
|
53
|
+
message = Message.new
|
|
54
|
+
message.contact_id = contact.id
|
|
55
|
+
message.mailing_id = self.id
|
|
56
|
+
message.change_status(:ready)
|
|
57
|
+
message.delay.deliver
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def deliver
|
|
61
|
+
Rails.logger.info "Starting to Process Mailing '#{subject}' ID:#{id}"
|
|
62
|
+
Lock.with_lock("mail_mgr_mailing_send[#{id}]") do |lock|
|
|
63
|
+
unless status.to_s.eql?('scheduled')
|
|
64
|
+
raise Exception.new("Mailing was not scheduled when job tried to run!")
|
|
65
|
+
end
|
|
66
|
+
unless scheduled_at <= Time.now
|
|
67
|
+
Rails.logger.info "Mailing is not scheduled to run until #{scheduled_at} rescheduling job!"
|
|
68
|
+
self.delay(run_at: scheduled_at).deliver
|
|
69
|
+
return true
|
|
70
|
+
end
|
|
71
|
+
change_status(:processing)
|
|
72
|
+
initialize_messages
|
|
73
|
+
messages.pending.each do |message|
|
|
74
|
+
if reload.status.to_s != 'processing'
|
|
75
|
+
Rails.logger.warn "Mailing #{id} is no longer in processing status it was changed to #{status} while running"
|
|
76
|
+
return false
|
|
77
|
+
end
|
|
78
|
+
begin
|
|
79
|
+
# use the cached mailing parts, set messages mailing to self
|
|
80
|
+
message.mailing=self
|
|
81
|
+
message.change_status(:processing)
|
|
82
|
+
message.deliver
|
|
83
|
+
message.change_status(:sent)
|
|
84
|
+
rescue => e
|
|
85
|
+
message.result = "Error: #{e.message} - #{e.backtrace.join("\n")}"
|
|
86
|
+
message.change_status(:failed)
|
|
87
|
+
end
|
|
88
|
+
Rails.logger.debug "Sleeping #{MailManager.sleep_time_between_messages} before next message"
|
|
89
|
+
sleep MailManager.sleep_time_between_messages
|
|
90
|
+
end
|
|
91
|
+
change_status(:completed) if status.to_s.eql?('processing')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def mailable
|
|
96
|
+
return @mailable if @mailable
|
|
97
|
+
return self unless mailable_type and mailable_id
|
|
98
|
+
@mailable = mailable_type.constantize.find(mailable_id)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.cleanse_source(source)
|
|
102
|
+
require 'iconv' unless String.method_defined?(:encode)
|
|
103
|
+
if String.method_defined?(:encode)
|
|
104
|
+
source.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
|
|
105
|
+
source.encode!('UTF-8', 'UTF-16')
|
|
106
|
+
else
|
|
107
|
+
ic = Iconv.new('UTF-8', 'UTF-8//IGNORE')
|
|
108
|
+
source = ic.iconv(source)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.substitute_values(source,substitutions)
|
|
113
|
+
substitutions.each_pair do |substitution,value|
|
|
114
|
+
if value.blank?
|
|
115
|
+
source.gsub!(/##{substitution}#([^#]*)#/,'\1') rescue source = self.cleanse_source(source).gsub(/##{substitution}#([^#]*)#/,'\1')
|
|
116
|
+
else
|
|
117
|
+
source.gsub!(/##{substitution}#[^#]*#/,value.to_s) rescue source = self.cleanse_source(source).gsub(/##{substitution}#[^#]*#/,value.to_s)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
if defined? MailManager::ContactableRegistry.respond_to?(:valid_contactable_substitutions)
|
|
121
|
+
MailManager::ContactableRegistry.valid_contactable_substitutions.
|
|
122
|
+
reject{|key| substitutions.keys.include?(key)}.each do |substitution|
|
|
123
|
+
source.gsub!(/##{substitution}#([^#]*)#/,'\1') rescue source = self.cleanse_source(source).gsub(/##{substitution}#([^#]*)#/,'\1')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
source
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def raw_parts
|
|
130
|
+
@raw_parts ||= mailable.mailable_parts
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def parts(substitutions={})
|
|
134
|
+
parts = []
|
|
135
|
+
raw_parts.each do |type,source|
|
|
136
|
+
parts << [type, Mailing.substitute_values(source.dup,substitutions)]
|
|
137
|
+
end
|
|
138
|
+
parts
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def mailable=(value)
|
|
142
|
+
return if value.nil?
|
|
143
|
+
self[:mailable_type] = value.class.name
|
|
144
|
+
self[:mailable_id] = value.id
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def mailable_class_and_id=(value)
|
|
148
|
+
return if value.nil?
|
|
149
|
+
parts = value.split(/_/)
|
|
150
|
+
self[:mailable_id] = parts.pop
|
|
151
|
+
self[:mailable_type] = parts.join('_')
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
#def mailable_attributes=(mailable_attributes={})
|
|
155
|
+
# mailable_attributes.each_pair do |key,value|
|
|
156
|
+
# end
|
|
157
|
+
#end
|
|
158
|
+
|
|
159
|
+
# creates all of the Messages that will be sent for this mailing
|
|
160
|
+
def initialize_messages
|
|
161
|
+
unless messages.length > 0
|
|
162
|
+
Rails.logger.info "Building mailing messages for mailing(#{id})"
|
|
163
|
+
transaction do
|
|
164
|
+
emails_hash = messages.select{|m| m.type.eql?('MailManager::Message')}.inject(Hash.new){|emails_hash,message| emails_hash.merge(Mailing.clean_email_address(message.email_address)=>1)}
|
|
165
|
+
mailing_lists.each do |mailing_list|
|
|
166
|
+
mailing_list.subscriptions.active.each do |subscription|
|
|
167
|
+
contact = subscription.contact
|
|
168
|
+
next if contact.nil? or contact.deleted?
|
|
169
|
+
email_address = Mailing.clean_email_address(contact.email_address)
|
|
170
|
+
if emails_hash.has_key?(email_address)
|
|
171
|
+
Rails.logger.info "Skipping duplicate address: #{email_address}"
|
|
172
|
+
else
|
|
173
|
+
Rails.logger.info "Adding #{email_address} to mailing #{subject}"
|
|
174
|
+
emails_hash[email_address] = 1
|
|
175
|
+
message = Message.new
|
|
176
|
+
message.subscription = subscription
|
|
177
|
+
message.contact = contact
|
|
178
|
+
message.mailing = self
|
|
179
|
+
message.save
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
save
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# clean up an email address for sending FIXME - maybe do a bit more
|
|
189
|
+
def self.clean_email_address(email_address)
|
|
190
|
+
email_address.downcase.strip
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# sends a test message for this mailing to the given address
|
|
194
|
+
def send_test_message(test_email_addresses)
|
|
195
|
+
test_email_addresses.split(/,/).each do |test_email_address|
|
|
196
|
+
puts "Creating test message for #{test_email_address}"
|
|
197
|
+
test_message = TestMessage.new(:test_email_address => test_email_address.strip)
|
|
198
|
+
test_message.mailing_id = self.id
|
|
199
|
+
test_message.save
|
|
200
|
+
test_message.delay.deliver
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# used in select helpers to identify this Mailing's Mailable
|
|
205
|
+
def mailable_thing_and_id
|
|
206
|
+
return '' if mailable.nil?
|
|
207
|
+
return "#{mailable.class.name}_#{mailable.id}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def mailing_list_ids=(mailing_list_ids)
|
|
211
|
+
mailing_list_ids.delete('')
|
|
212
|
+
self.mailing_lists = mailing_list_ids.collect{|mailing_list_id| MailingList.find_by_id(mailing_list_id)}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def can_pause?
|
|
216
|
+
['processing'].include?(status.to_s)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def can_edit?
|
|
220
|
+
['pending','scheduled','paused'].include?(status.to_s)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def can_cancel?
|
|
224
|
+
['pending','scheduled','processing','paused','resumed'].include?(status.to_s)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def can_resume?
|
|
228
|
+
['paused'].include?(status.to_s)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def can_schedule?
|
|
232
|
+
['pending'].include?(status.to_s)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def schedule
|
|
236
|
+
raise "Unable to schedule" unless can_schedule?
|
|
237
|
+
change_status('scheduled')
|
|
238
|
+
delay(run_at: scheduled_at).deliver
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def cancel
|
|
242
|
+
raise "Unable to cancel" unless can_cancel?
|
|
243
|
+
change_status('pending')
|
|
244
|
+
# Delayed::Job.active.find(:all, :conditions => ["handler like ?","MailMgr::Mailing"])
|
|
245
|
+
|
|
246
|
+
#Changing this to return only the jobs that match the id so I don't have to parse with YAML ... seems logical
|
|
247
|
+
mailing_jobs = Delayed::Job.find(:all, :conditions => ["handler like ?","%MailMgr::Mailing%"] || ["handler like ?", "%id: {job_mailing_id.to_i}\n%"])
|
|
248
|
+
#mailing_jobs = Delayed::Job.active.find(:all, :conditions => ["handler like ?","%MailMgr::Mailing%"])
|
|
249
|
+
mailing_jobs.each do |job|
|
|
250
|
+
#job_mailing_id = YAML::load(job.handler).object.split(':').last
|
|
251
|
+
#logger.debug "Job mailing id: #{job_mailing_id} - This mailing id: #{self.id} - do they match: #{job_mailing_id.to_i == self.id.to_i}"
|
|
252
|
+
job.destroy #if job_mailing_id.to_i == self.id.to_i
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def resume
|
|
257
|
+
raise "Unable to resume" unless can_resume?
|
|
258
|
+
change_status('resumed')
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def pause
|
|
262
|
+
raise "Unable to pause" unless can_pause?
|
|
263
|
+
change_status('paused')
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|