msg 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.
Files changed (119) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +9 -0
  3. data/Rakefile +35 -0
  4. data/app/assets/images/msg/message.png +0 -0
  5. data/app/assets/images/msg/ping.png +0 -0
  6. data/app/assets/images/msg/sending.png +0 -0
  7. data/app/assets/javascripts/lib/jquery.peity.js +261 -0
  8. data/app/assets/javascripts/msg.js.coffee +6 -0
  9. data/app/assets/stylesheets/msg.css.sass +218 -0
  10. data/app/controllers/msg/bounces_controller.rb +67 -0
  11. data/app/controllers/msg/engine_controller.rb +23 -0
  12. data/app/controllers/msg/envelopes_controller.rb +22 -0
  13. data/app/controllers/msg/messages_controller.rb +85 -0
  14. data/app/controllers/msg/sendings_controller.rb +58 -0
  15. data/app/helpers/msg/msg_helper.rb +40 -0
  16. data/app/mailers/msg/devise_mailer.rb +20 -0
  17. data/app/mailers/msg/msg_mailer.rb +23 -0
  18. data/app/models/msg/bounce.rb +19 -0
  19. data/app/models/msg/envelope.rb +56 -0
  20. data/app/models/msg/message.rb +26 -0
  21. data/app/models/msg/sending.rb +54 -0
  22. data/app/views/layouts/msg/default.html.haml +30 -0
  23. data/app/views/layouts/msg/email.html.haml +10 -0
  24. data/app/views/msg/bounces/_listing.html.haml +5 -0
  25. data/app/views/msg/bounces/_report.html.haml +10 -0
  26. data/app/views/msg/bounces/index.html.haml +5 -0
  27. data/app/views/msg/devise_mailer/confirmation_instructions.en.html.haml +1 -0
  28. data/app/views/msg/devise_mailer/reset_password_instructions.en.html.haml +1 -0
  29. data/app/views/msg/envelopes/_listing.html.haml +5 -0
  30. data/app/views/msg/messages/_action_menu.html.haml +10 -0
  31. data/app/views/msg/messages/_form.html.haml +38 -0
  32. data/app/views/msg/messages/_instructions.html.haml +28 -0
  33. data/app/views/msg/messages/_message.html.haml +11 -0
  34. data/app/views/msg/messages/_saved_messages.html.haml +2 -0
  35. data/app/views/msg/messages/_transactional_messages.html.haml +3 -0
  36. data/app/views/msg/messages/edit.html.haml +7 -0
  37. data/app/views/msg/messages/index.html.haml +23 -0
  38. data/app/views/msg/messages/new.html.haml +7 -0
  39. data/app/views/msg/messages/preview.html.haml +4 -0
  40. data/app/views/msg/messages/show.html.haml +25 -0
  41. data/app/views/msg/msg_mailer/message_in_envelope.html.haml +2 -0
  42. data/app/views/msg/sendings/_form.html.haml +45 -0
  43. data/app/views/msg/sendings/_listing.html.haml +17 -0
  44. data/app/views/msg/sendings/_report.html.haml +8 -0
  45. data/app/views/msg/sendings/_sending.html.haml +22 -0
  46. data/app/views/msg/sendings/_sendings.html.haml +2 -0
  47. data/app/views/msg/sendings/index.html.haml +8 -0
  48. data/app/views/msg/sendings/new.html.haml +9 -0
  49. data/app/views/msg/sendings/review.html.haml +2 -0
  50. data/app/views/msg/sendings/show.html.haml +50 -0
  51. data/app/views/msg/shared/_toolbar.html.haml +17 -0
  52. data/config/cucumber.yml +8 -0
  53. data/config/locales/en.yml +76 -0
  54. data/config/routes.rb +26 -0
  55. data/db/migrate/20130320141926_msg_data.rb +54 -0
  56. data/db/migrate/20130327134754_sending_circumstances.rb +6 -0
  57. data/lib/msg.rb +88 -0
  58. data/lib/msg/engine.rb +23 -0
  59. data/lib/msg/receivers.rb +64 -0
  60. data/lib/msg/version.rb +3 -0
  61. data/lib/tasks/cucumber.rake +65 -0
  62. data/lib/tasks/msg_tasks.rake +4 -0
  63. data/spec/controllers/msg/bounces_controller_spec.rb +48 -0
  64. data/spec/controllers/msg/envelopes_controller_spec.rb +22 -0
  65. data/spec/controllers/msg/messages_controller_spec.rb +9 -0
  66. data/spec/controllers/msg/sendings_controller_spec.rb +47 -0
  67. data/spec/dummy/README.rdoc +261 -0
  68. data/spec/dummy/Rakefile +7 -0
  69. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  70. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  71. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  72. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  73. data/spec/dummy/app/models/user.rb +14 -0
  74. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  75. data/spec/dummy/config.ru +4 -0
  76. data/spec/dummy/config/application.rb +59 -0
  77. data/spec/dummy/config/boot.rb +10 -0
  78. data/spec/dummy/config/database.yml +25 -0
  79. data/spec/dummy/config/environment.rb +5 -0
  80. data/spec/dummy/config/environments/development.rb +37 -0
  81. data/spec/dummy/config/environments/production.rb +67 -0
  82. data/spec/dummy/config/environments/test.rb +41 -0
  83. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  84. data/spec/dummy/config/initializers/devise.rb +285 -0
  85. data/spec/dummy/config/initializers/inflections.rb +15 -0
  86. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  87. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  88. data/spec/dummy/config/initializers/session_store.rb +8 -0
  89. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  90. data/spec/dummy/config/locales/en.yml +5 -0
  91. data/spec/dummy/config/routes.rb +9 -0
  92. data/spec/dummy/db/development.sqlite3 +0 -0
  93. data/spec/dummy/db/migrate/2013032210540_users.rb +49 -0
  94. data/spec/dummy/db/migrate/20130326093048_msg_data.msg.rb +55 -0
  95. data/spec/dummy/db/schema.rb +96 -0
  96. data/spec/dummy/db/test.sqlite3 +0 -0
  97. data/spec/dummy/log/development.log +143 -0
  98. data/spec/dummy/log/test.log +1022 -0
  99. data/spec/dummy/public/404.html +26 -0
  100. data/spec/dummy/public/422.html +26 -0
  101. data/spec/dummy/public/500.html +25 -0
  102. data/spec/dummy/public/favicon.ico +0 -0
  103. data/spec/dummy/script/rails +6 -0
  104. data/spec/factories/msg/bounces.rb +9 -0
  105. data/spec/factories/msg/envelopes.rb +10 -0
  106. data/spec/factories/msg/messages.rb +10 -0
  107. data/spec/factories/msg/sendings.rb +7 -0
  108. data/spec/factories/users.rb +11 -0
  109. data/spec/helpers/msg/msg_helper_spec.rb +17 -0
  110. data/spec/lib/msg_spec.rb +15 -0
  111. data/spec/mailers/msg/devise_mailer_spec.rb +21 -0
  112. data/spec/mailers/msg/msg_mailer_spec.rb +36 -0
  113. data/spec/models/msg/bounce_spec.rb +19 -0
  114. data/spec/models/msg/envelope_spec.rb +62 -0
  115. data/spec/models/msg/message_spec.rb +19 -0
  116. data/spec/models/msg/sending_spec.rb +10 -0
  117. data/spec/routing/sendings_routing_spec.rb +31 -0
  118. data/spec/spec_helper.rb +70 -0
  119. metadata +446 -0
@@ -0,0 +1,67 @@
1
+ require 'open-uri'
2
+
3
+ module Msg
4
+ class BouncesController < Msg::EngineController
5
+ respond_to :json
6
+
7
+ # this is pinged from Amazon SNS with a JSON body something like this:
8
+ #
9
+ # {
10
+ # "notificationType":"Bounce",
11
+ # "bounce":{
12
+ # "bounceType":"Permanent",
13
+ # "reportingMTA":"dns; email.example.com",
14
+ # "bouncedRecipients":[
15
+ # {
16
+ # "emailAddress":"username@example.com",
17
+ # "status":"5.1.1",
18
+ # "action":"failed",
19
+ # "diagnosticCode":"smtp; 550 5.1.1 <username@example.com>... User"
20
+ # }
21
+ # ],
22
+ # "bounceSubType":"General",
23
+ # "timestamp":"2012-06-19T01:07:52.000Z",
24
+ # "feedbackId":"00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa068a-000000"
25
+ # },
26
+ # "mail":{
27
+ # "timestamp":"2012-06-19T01:05:45.000Z",
28
+ # "source":"sender@example.com",
29
+ # "messageId":"00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa0680-000000",
30
+ # "destination":[
31
+ # "username@example.com"
32
+ # ]
33
+ # }
34
+ # }
35
+ #
36
+ #
37
+ # When the subscription is first set up they will also POST us a SubscriptionConfirmation,
38
+ # upon which we have to GET the given url to confirm that we can follow orders.
39
+ #
40
+ def create
41
+ if request.headers['x-amz-sns-message-type'] == 'SubscriptionConfirmation'
42
+ response = HTTParty.get(params['SubscribeURL'])
43
+ head :ok
44
+
45
+ else
46
+ mail = params['Message']['mail']
47
+ bounce = params['Message']['bounce']
48
+ if envelope = Msg::Envelope.find_by_email_id(mail['messageId'])
49
+ envelope.bounces.create({
50
+ :bounce_type => bounce['bounceType'],
51
+ :bounce_subtype => bounce['bounceSubType'],
52
+ :reporter => bounce['reportingMTA'],
53
+ :raw_message => JSON.dump(bounce)
54
+ })
55
+ end
56
+ head :ok
57
+ end
58
+ end
59
+
60
+ protected
61
+
62
+ def build_bounce
63
+ @bounce = Msg::Bounce.new(params[:bounce])
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,23 @@
1
+ module Msg
2
+ class EngineController < ::ApplicationController
3
+
4
+ before_filter :authenticate_user!
5
+ before_filter :require_admin!
6
+ layout :no_layout_if_pjax
7
+
8
+ protected
9
+
10
+ def require_admin!
11
+ raise Msg::PermissionDenied unless current_user.admin?
12
+ end
13
+
14
+ def no_layout_if_pjax
15
+ if request.headers['X-PJAX']
16
+ false
17
+ else
18
+ Msg.layout
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ module Msg
2
+ class EnvelopesController < Msg::EngineController
3
+ respond_to :png
4
+ skip_before_filter :authenticate_user!, :only => [:show]
5
+ skip_before_filter :require_admin!, :only => [:show]
6
+ before_filter :get_envelope, :only => [:show]
7
+
8
+ # envelopes#show is pinged when a message is opened.
9
+ #
10
+ def show
11
+ @envelope.open!
12
+ redirect_to view_context.image_path('msg/ping.png')
13
+ end
14
+
15
+ protected
16
+
17
+ def get_envelope
18
+ @envelope = Msg::Envelope.find(params[:id])
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,85 @@
1
+ module Msg
2
+ class MessagesController < Msg::EngineController
3
+ respond_to :html, :js
4
+
5
+ before_filter :get_message, :only => [:show, :edit, :update, :destroy, :preview]
6
+ before_filter :build_message, :only => [:new, :create]
7
+ before_filter :get_transactional_messages, :only => [:index, :transactional]
8
+ before_filter :get_saved_messages, :only => [:index, :saved]
9
+ before_filter :get_receiver, :only => [:show, :preview]
10
+
11
+ def index
12
+ respond_with @saved_messages, @transactional_messages
13
+ end
14
+
15
+ def saved
16
+ respond_with @saved_messages
17
+ end
18
+
19
+ def transactional
20
+ respond_with @transactional_messages
21
+ end
22
+
23
+ def show
24
+ respond_with @message
25
+ end
26
+
27
+ def preview
28
+ respond_with @message, :layout => Msg.email_layout
29
+ end
30
+
31
+ def new
32
+ respond_with @message
33
+ end
34
+
35
+ def create
36
+ @message.update_attributes(params[:message])
37
+ respond_with @message
38
+ end
39
+
40
+ def edit
41
+ respond_with @message
42
+ end
43
+
44
+ def update
45
+ @message.update_attributes(params[:message])
46
+ respond_with @message
47
+ end
48
+
49
+ def destroy
50
+ @message.destroy
51
+ head :ok
52
+ end
53
+
54
+ protected
55
+
56
+ def build_message
57
+ @message = Msg::Message.new(params[:message])
58
+ end
59
+
60
+ def get_message
61
+ @message = Msg::Message.find(params[:id])
62
+ end
63
+
64
+ def get_transactional_messages
65
+ @transactional_messages = Msg::Message.transactional
66
+ end
67
+
68
+ def get_saved_messages
69
+ @show = params[:show] || 10
70
+ @page = params[:page] || 1
71
+ @saved_messages = Msg::Message.saved.page(@page).per(@show)
72
+ end
73
+
74
+ def get_receiver
75
+ klass = params[:receiver_type] || Msg.receiving_classes.first
76
+ if id = params[:receiver_id]
77
+ @receiver = klass.classify.constantize.find(id)
78
+ else
79
+ @receiver = klass.classify.constantize.first
80
+ end
81
+ raise ActiveRecord::RecordNotFound, "Cannot find a valid receiver for whom to preview message." unless @receiver
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,58 @@
1
+ module Msg
2
+ class SendingsController < Msg::EngineController
3
+ respond_to :html, :js
4
+
5
+ before_filter :get_message, :only => [:new, :create]
6
+ before_filter :get_sending, :only => [:show, :review]
7
+ before_filter :build_sending, :only => [:new, :create]
8
+ before_filter :get_sendings, :only => [:index]
9
+
10
+ def index
11
+ respond_with @sendings do |format|
12
+ format.js { render :partial => "report"}
13
+ end
14
+ end
15
+
16
+ def show
17
+ respond_with @sending
18
+ end
19
+
20
+ def review
21
+ respond_with @sending, :layout => Msg.email_layout
22
+ end
23
+
24
+ def new
25
+ respond_with @sending
26
+ end
27
+
28
+ def create
29
+ @sending.update_attributes(params[:sending])
30
+ respond_with @sending
31
+ end
32
+
33
+ def destroy
34
+ @sending.destroy
35
+ head :ok
36
+ end
37
+
38
+ protected
39
+
40
+ def get_message
41
+ @message = Msg::Message.find(params[:message_id])
42
+ end
43
+
44
+ def build_sending
45
+ @sending = @message.sendings.build
46
+ end
47
+
48
+ def get_sending
49
+ @sending = Msg::Sending.find(params[:id])
50
+ end
51
+
52
+ def get_sendings
53
+ @show = params[:show] || 10
54
+ @page = params[:page] || 1
55
+ @sendings = Msg::Sending.order("created_at DESC").page(@page).per(@show)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,40 @@
1
+ module Msg
2
+ module MsgHelper
3
+ def shorten(text, length=64)
4
+ length = length[:length] if length.is_a?(Hash)
5
+ truncate(strip_tags(text), {:length => length, :separator => " "})
6
+ end
7
+
8
+ def action_menulink(thing, html_options={})
9
+ classname = thing.class.to_s.downcase.underscore.split('/').last
10
+ html_options.reverse_merge!({
11
+ :class => "",
12
+ :data => {:menu => "#{classname}_#{thing.id}"}
13
+ })
14
+ html_options[:class] << " menu"
15
+ link_to t(:edit), "#", html_options if editable?(thing)
16
+ end
17
+
18
+ def action_menu(thing, locals={})
19
+ if editable?(thing)
20
+ type = thing.class.to_s.downcase.underscore
21
+ classname = type.split('/').last
22
+ locals[classname.to_sym] = thing
23
+ render :partial => "#{type.pluralize}/action_menu", :locals => locals
24
+ end
25
+ end
26
+
27
+ def visible?(thing)
28
+ admin? || privileged? || current_user.can_see?(thing)
29
+ end
30
+
31
+ def editable?(thing)
32
+ admin? || current_user == thing.created_by
33
+ end
34
+
35
+ def deletable?(thing)
36
+ admin? || current_user == thing.created_by
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ module Msg
2
+ class DeviseMailer < Devise::Mailer
3
+ default from: Msg.default_from_address, :bcc => Msg.email_bcc
4
+ layout Msg.email_layout
5
+
6
+ def confirmation_instructions(receiver, opts={})
7
+ @message = Msg::Message.find_by_function('confirmation_instructions')
8
+ @contents = @message.render_for(receiver)
9
+ super
10
+ end
11
+
12
+ def reset_password_instructions(receiver, opts={})
13
+ @message = Msg::Message.find_by_function('reset_password_instructions')
14
+ @contents = @message.render_for(receiver)
15
+ super
16
+ end
17
+
18
+
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module Msg
2
+ class MsgMailer < ActionMailer::Base
3
+ default from: Msg.default_from, :bcc => Msg.email_bcc
4
+ layout Msg.email_layout
5
+
6
+ # we send all messages through SES and use SNS to notify of bounces, so all the
7
+ # return-path and VERP and other gubbins that we would normally need can be omitted
8
+ #
9
+ def message_in_envelope(envelope)
10
+ headers['Message-ID'] = envelope.email_id
11
+ @envelope = envelope
12
+ @message = envelope.message
13
+ @receiver = envelope.receiver
14
+ @contents = envelope.contents
15
+ mail({
16
+ :to => %{"#{@receiver.name}" <#{@receiver.email}>},
17
+ :from => envelope.from_address,
18
+ :subject => envelope.subject
19
+ })
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module Msg
2
+ class Bounce < ActiveRecord::Base
3
+ attr_accessible :envelope, :bounce_type, :bounce_subtype, :raw_message, :reporter
4
+ belongs_to :envelope
5
+ validates :envelope, :presence => true
6
+
7
+ scope :fatal, where(:bounce_type => "Permanent")
8
+ scope :transient, where(:bounce_type => "Transient")
9
+
10
+ def fatal?
11
+ bounce_type == 'Permanent'
12
+ end
13
+
14
+ def transient?
15
+ bounce_type == 'Transient'
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ module Msg
2
+ class Envelope < ActiveRecord::Base
3
+ attr_accessible :receiver, :sending
4
+
5
+ belongs_to :sending
6
+ belongs_to :receiver, :polymorphic => true
7
+ has_many :bounces
8
+
9
+ validates :receiver, :presence => true
10
+ validates :sending, :presence => true
11
+ after_create :send_email
12
+
13
+ scope :opened, where("opened_at IS NOT NULL")
14
+
15
+ scope :bounced, select("msg_envelopes.*")
16
+ .joins("INNER JOIN msg_bounces as mb ON mb.envelope_id = msg_envelopes.id")
17
+ .having("count(mb.id) > 0")
18
+
19
+ def message
20
+ sending.message
21
+ end
22
+
23
+ def open!
24
+ update_column(:opened_at, Time.now)
25
+ end
26
+
27
+ def status
28
+ if opened_at?
29
+ "read"
30
+ elsif bounces.any?
31
+ "bounced"
32
+ else
33
+ "unread"
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def send_email
40
+ # Record sending parameters for later reference. The message or receiver properties might change after this is sent.
41
+ self.email_id = "#{self.id}@#{Msg.sending_domain}"
42
+ self.subject = message.subject
43
+ self.from_address = message.from
44
+ self.to_address = receiver.email
45
+ # render the message for our receiver
46
+ self.contents = message.render_for(receiver)
47
+ # send it
48
+ Msg::MsgMailer.message_in_envelope(self).deliver
49
+ # note sending time
50
+ self.sent_at = Time.now
51
+ # and save our changes
52
+ self.save!
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,26 @@
1
+ require 'mustache'
2
+
3
+ module Msg
4
+ class Message < ActiveRecord::Base
5
+ attr_accessible :subject, :body, :function, :description, :transactional, :saved, :from_name, :from_address
6
+
7
+ has_many :sendings
8
+ belongs_to :created_by, :class_name => Msg.user_class
9
+
10
+ scope :transactional, where(:transactional => true)
11
+ scope :saved, where(:saved => true, :transactional => false)
12
+ scope :unsaved, where(:saved => false)
13
+
14
+ def from
15
+ name = from_name? ? from_name : Msg.default_from_name
16
+ address = from_address? ? from_address : Msg.default_from_address
17
+ "#{name} <#{address}>"
18
+ end
19
+
20
+ def render_for(receiver)
21
+ values = receiver.for_email.reverse_merge(Msg.email_values)
22
+ Mustache.render(body, values)
23
+ end
24
+
25
+ end
26
+ end