diff --git a/Gemfile.lock b/Gemfile.lock index 7c5b42ef..c41d59bd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,7 +15,9 @@ PATH remote: plugins/messages specs: foodsoft_messages (0.0.1) + base32 deface (~> 1.0.0) + mail rails PATH @@ -71,6 +73,7 @@ GEM addressable (2.4.0) arel (6.0.3) attribute_normalizer (1.2.0) + base32 (0.3.2) better_errors (2.1.1) coderay (>= 1.0.0) erubis (>= 2.6.6) @@ -534,3 +537,6 @@ DEPENDENCIES uglifier (>= 1.0.3) web-console (~> 2.0) whenever + +BUNDLED WITH + 1.10.6 diff --git a/config/app_config.yml.SAMPLE b/config/app_config.yml.SAMPLE index f9b9bd27..b7e64ee2 100644 --- a/config/app_config.yml.SAMPLE +++ b/config/app_config.yml.SAMPLE @@ -116,6 +116,9 @@ default: &defaults # email address to be used as sender email_sender: foodsoft@foodcoop.test + # domain to be used for reply emails + #reply_email_domain: reply.foodcoop.test + # If your foodcoop uses a mailing list instead of internal messaging system #mailing_list: list@example.org #mailing_list_subscribe: list-subscribe@example.org diff --git a/db/migrate/20160226000000_add_email_to_message.foodsoft_messages_engine.rb b/db/migrate/20160226000000_add_email_to_message.foodsoft_messages_engine.rb new file mode 100644 index 00000000..ad107db2 --- /dev/null +++ b/db/migrate/20160226000000_add_email_to_message.foodsoft_messages_engine.rb @@ -0,0 +1,7 @@ +# This migration comes from foodsoft_messages_engine (originally 20160226000000) +class AddEmailToMessage < ActiveRecord::Migration + def change + add_column :messages, :salt, :string + add_column :messages, :received_email, :binary, :limit => 1.megabyte + end +end diff --git a/db/schema.rb b/db/schema.rb index 983e6992..75953d6c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -188,6 +188,8 @@ ActiveRecord::Schema.define(version: 20160217194036) do t.datetime "created_at" t.integer "reply_to", limit: 4 t.integer "group_id", limit: 4 + t.string "salt" + t.binary "received_email" end create_table "order_articles", force: :cascade do |t| diff --git a/lib/foodsoft_config.rb b/lib/foodsoft_config.rb index e5aded62..c5e4e62a 100644 --- a/lib/foodsoft_config.rb +++ b/lib/foodsoft_config.rb @@ -80,6 +80,10 @@ class FoodsoftConfig setup_database end + def select_multifoodcoop(foodcoop) + select_foodcoop foodcoop if config[:multi_coop_install] + end + # Return configuration value for the currently selected foodcoop. # # First tries to read configuration from the database (cached), @@ -134,15 +138,20 @@ class FoodsoftConfig keys.map(&:to_s).uniq end + # @return [Array] Valid names of foodcoops. + def foodcoops + if config[:multi_coop_install] + APP_CONFIG.keys.reject { |coop| coop =~ /^(default|development|test|production)$/ } + else + config[:default_scope] + end + end + # Loop through each foodcoop and executes the given block after setup config and database def each_coop - if config[:multi_coop_install] - APP_CONFIG.keys.reject { |coop| coop =~ /^(default|development|test|production)$/ }.each do |coop| - select_foodcoop coop - yield coop - end - else - yield config[:default_scope] + foodcoops.each do |coop| + select_multifoodcoop coop + yield coop end end diff --git a/plugins/messages/README.md b/plugins/messages/README.md index 4af6235f..fd289467 100644 --- a/plugins/messages/README.md +++ b/plugins/messages/README.md @@ -14,5 +14,9 @@ gem 'foodsoft_messages', path: 'lib/foodsoft_messages' This plugin introduces the foodcoop config option `use_messages`, which can be set to `false` to disable messages. May be useful in multicoop deployments. +To allow members to respond to messages via email, see the config option +`reply_email_domain` and the rake task `foodsoft:parse_reply_email`. We need to +add some documentation on setting it up, though. + This plugin is part of the foodsoft package and uses the GPL-3 license (see foodsoft's LICENSE for the full license text). diff --git a/plugins/messages/app/mailers/messages_mailer.rb b/plugins/messages/app/mailers/messages_mailer.rb index f3003686..a793d83e 100644 --- a/plugins/messages/app/mailers/messages_mailer.rb +++ b/plugins/messages/app/mailers/messages_mailer.rb @@ -4,9 +4,17 @@ class MessagesMailer < Mailer set_foodcoop_scope @message = message + reply_email_domain = FoodsoftConfig[:reply_email_domain] + if reply_email_domain + hash = message.mail_hash_for_user recipient + reply_to = "Foodsoft <#{FoodsoftConfig.scope}.#{message.id}.#{recipient.id}.#{hash}@#{reply_email_domain}>" + else + reply_to = "#{show_user(message.sender)} <#{message.sender.email}>" + end + mail subject: "[#{FoodsoftConfig[:name]}] " + message.subject, to: recipient.email, from: "#{show_user(message.sender)} via Foodsoft <#{FoodsoftConfig[:email_sender]}>", - reply_to: "#{show_user(message.sender)} <#{message.sender.email}>" + reply_to: reply_to end end diff --git a/plugins/messages/app/models/message.rb b/plugins/messages/app/models/message.rb index af768e0b..bfe00589 100644 --- a/plugins/messages/app/models/message.rb +++ b/plugins/messages/app/models/message.rb @@ -1,3 +1,5 @@ +require "base32" + class Message < ActiveRecord::Base belongs_to :sender, :class_name => "User", :foreign_key => "sender_id" belongs_to :group, :class_name => "Group", :foreign_key => "group_id" @@ -23,6 +25,7 @@ class Message < ActiveRecord::Base validates_length_of :subject, :in => 1..255 validates_inclusion_of :email_state, :in => EMAIL_STATE.values + before_create :create_salt before_validation :clean_up_recipient_ids, :on => :create def self.deliver(message_id) @@ -61,8 +64,18 @@ class Message < ActiveRecord::Base add_recipients([user]) end + def mail_hash_for_user(user) + digest = Digest::SHA1.new + digest.update self.id.to_s + digest.update ":" + digest.update salt + digest.update ":" + digest.update user.id.to_s + Base32.encode digest.digest + end + # Returns true if this message is a system message, i.e. was sent automatically by Foodsoft itself. - def system_message? + def system_message? self.sender_id.nil? end @@ -94,6 +107,10 @@ class Message < ActiveRecord::Base def is_readable_for?(user) !private || sender == user || recipients_ids.include?(user.id) end + + private + + def create_salt + self.salt = [Array.new(6){rand(256).chr}.join].pack("m").chomp + end end - - diff --git a/plugins/messages/db/migrate/20160226000000_add_email_to_message.rb b/plugins/messages/db/migrate/20160226000000_add_email_to_message.rb new file mode 100644 index 00000000..c7b4c6f5 --- /dev/null +++ b/plugins/messages/db/migrate/20160226000000_add_email_to_message.rb @@ -0,0 +1,6 @@ +class AddEmailToMessage < ActiveRecord::Migration + def change + add_column :messages, :salt, :string + add_column :messages, :received_email, :binary, :limit => 1.megabyte + end +end diff --git a/plugins/messages/foodsoft_messages.gemspec b/plugins/messages/foodsoft_messages.gemspec index 0e244563..1cad6bd7 100644 --- a/plugins/messages/foodsoft_messages.gemspec +++ b/plugins/messages/foodsoft_messages.gemspec @@ -17,7 +17,9 @@ Gem::Specification.new do |s| s.test_files = Dir["test/**/*"] s.add_dependency "rails" + s.add_dependency "base32" s.add_dependency "deface", "~> 1.0.0" + s.add_dependency "mail" s.add_development_dependency "sqlite3" end diff --git a/plugins/messages/lib/tasks/foodsoft.rake b/plugins/messages/lib/tasks/foodsoft.rake new file mode 100644 index 00000000..69a0d7b7 --- /dev/null +++ b/plugins/messages/lib/tasks/foodsoft.rake @@ -0,0 +1,60 @@ +require "mail" + +namespace :foodsoft do + desc "Parse incoming email on stdin (options: RECIPIENT=f.1.2.a1b2c3d3e5)" + task :parse_reply_email => :environment do + m = /(?[^@]*)\.(?\d+)\.(?\d+)\.(?\w+)(@(?.*))?/.match(ENV['RECIPIENT']) + + raise "RECIPIENT is missing or has an invalid format" if m.nil? + raise "Foodcoop '#{m[:foodcoop]}' could not be found" unless FoodsoftConfig.foodcoops.include? m[:foodcoop] + + FoodsoftConfig.select_multifoodcoop m[:foodcoop] + original_message = Message.find_by_id(m[:message_id]) + user = User.find_by_id(m[:user_id]) + + raise "Message could not be found" if original_message.nil? + raise "User could not be found" if user.nil? + + hash = original_message.mail_hash_for_user user + raise "Hash does not match expectations" unless hash.casecmp(m[:hash]) == 0 + + received_email = STDIN.read + mail = Mail.new received_email + + mail_part = nil + if mail.multipart? + for part in mail.parts + mail_part = part if MIME::Type.simplified(part.content_type) == "text/plain" + end + else + mail_part = mail + end + + body = mail_part.body.decoded + unless mail_part.content_type_parameters.nil? + body = body.force_encoding mail_part.content_type_parameters[:charset] + end + + message = user.send_messages.new body: body, + group: original_message.group, + private: original_message.private, + received_email: received_email, + subject: mail.subject + if original_message.reply_to + message.reply_to_message = original_message.reply_to_message + else + message.reply_to_message = original_message + end + message.add_recipients original_message.recipients + message.add_recipients [original_message.sender] + + message.save! + Resque.enqueue(MessageNotifier, FoodsoftConfig.scope, "message_deliver", message.id) + rake_say "Handled reply email from #{user.display}." + end +end + +# Helper +def rake_say(message) + puts message unless Rake.application.options.silent +end