diff --git a/Gemfile.lock b/Gemfile.lock index 5f968a9c..735f7e41 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,7 +17,9 @@ PATH foodsoft_messages (0.0.1) base32 deface (~> 1.0.0) + gserver mail + mini-smtp-server rails PATH @@ -162,6 +164,7 @@ GEM git-version-bump (0.15.1) globalid (0.3.6) activesupport (>= 4.1.0) + gserver (0.0.1) haml (4.0.7) tilt haml-rails (0.9.0) @@ -230,6 +233,7 @@ GEM mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) + mini-smtp-server (0.0.2) mini_portile2 (2.1.0) minitest (5.9.0) mono_logger (1.1.0) diff --git a/plugins/messages/README.md b/plugins/messages/README.md index fd289467..15078891 100644 --- a/plugins/messages/README.md +++ b/plugins/messages/README.md @@ -14,9 +14,14 @@ 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. +To allow members to respond to messages via email, you need the set the config +option `reply_email_domain` and handle incoming mails via one of the following +rake tasks. `foodsoft:reply_email_smtp_server` starts an SMTP server on the +port given via the environment variable `PORT` and listens until a shutdown +signal is received. If there is already a SMTP server for handling incoming +mails you can also feed every mail via a call to `foodsoft:parse_reply_email` +into foodsoft. It expects the address given in the `MAIL FROM` command via +SMTP in the environment variable `RECIPIENT` and the mail body as `STDIN`. 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/foodsoft_messages.gemspec b/plugins/messages/foodsoft_messages.gemspec index 1cad6bd7..05626f94 100644 --- a/plugins/messages/foodsoft_messages.gemspec +++ b/plugins/messages/foodsoft_messages.gemspec @@ -20,6 +20,8 @@ Gem::Specification.new do |s| s.add_dependency "base32" s.add_dependency "deface", "~> 1.0.0" s.add_dependency "mail" + s.add_dependency "mini-smtp-server" + s.add_dependency "gserver" s.add_development_dependency "sqlite3" end diff --git a/plugins/messages/lib/tasks/foodsoft.rake b/plugins/messages/lib/tasks/foodsoft.rake index 69a0d7b7..c753ab6c 100644 --- a/plugins/messages/lib/tasks/foodsoft.rake +++ b/plugins/messages/lib/tasks/foodsoft.rake @@ -1,57 +1,83 @@ require "mail" +require "mini-smtp-server" + +class ReplyEmailSmtpServer < MiniSmtpServer + + def new_message_event(message_hash) + m = /<(?[^<>]+)>/.match(message_hash[:to]) + raise "invalid format for RCPT TO" if m.nil? + hande_mail(m[:recipient], message_hash[:data]) + rescue => error + rake_say error.message + end + +end 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}." + hande_mail(ENV['RECIPIENT'], STDIN.read) end + + desc "Start STMP server for incoming email (options: PORT=25, HOST=0.0.0.0)" + task :reply_email_smtp_server => :environment do + port = ENV['PORT'].to_i + host = ENV['HOST'] + rake_say "Started SMTP server for incomming email on port #{port}." + server = ReplyEmailSmtpServer.new(port, host) + server.start + server.join + end +end + +def hande_mail(recipient, received_email) + m = /(?[^@]*)\.(?\d+)\.(?\d+)\.(?\w+)(@(?.*))?/.match(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 + + 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 # Helper