Merge pull request #459 from foodcoop1040/smtp

Add SMTP server for handling reply emails
This commit is contained in:
wvengen 2017-02-15 11:31:17 +01:00 committed by GitHub
commit 504c8b6192
4 changed files with 88 additions and 51 deletions

View File

@ -17,7 +17,9 @@ PATH
foodsoft_messages (0.0.1) foodsoft_messages (0.0.1)
base32 base32
deface (~> 1.0.0) deface (~> 1.0.0)
gserver
mail mail
mini-smtp-server
rails rails
PATH PATH
@ -162,6 +164,7 @@ GEM
git-version-bump (0.15.1) git-version-bump (0.15.1)
globalid (0.3.6) globalid (0.3.6)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
gserver (0.0.1)
haml (4.0.7) haml (4.0.7)
tilt tilt
haml-rails (0.9.0) haml-rails (0.9.0)
@ -230,6 +233,7 @@ GEM
mime-types (3.1) mime-types (3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mini-smtp-server (0.0.2)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
minitest (5.9.0) minitest (5.9.0)
mono_logger (1.1.0) mono_logger (1.1.0)

View File

@ -14,9 +14,14 @@ gem 'foodsoft_messages', path: 'lib/foodsoft_messages'
This plugin introduces the foodcoop config option `use_messages`, which can be This plugin introduces the foodcoop config option `use_messages`, which can be
set to `false` to disable messages. May be useful in multicoop deployments. 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 To allow members to respond to messages via email, you need the set the config
`reply_email_domain` and the rake task `foodsoft:parse_reply_email`. We need to option `reply_email_domain` and handle incoming mails via one of the following
add some documentation on setting it up, though. 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 This plugin is part of the foodsoft package and uses the GPL-3 license (see
foodsoft's LICENSE for the full license text). foodsoft's LICENSE for the full license text).

View File

@ -20,6 +20,8 @@ Gem::Specification.new do |s|
s.add_dependency "base32" s.add_dependency "base32"
s.add_dependency "deface", "~> 1.0.0" s.add_dependency "deface", "~> 1.0.0"
s.add_dependency "mail" s.add_dependency "mail"
s.add_dependency "mini-smtp-server"
s.add_dependency "gserver"
s.add_development_dependency "sqlite3" s.add_development_dependency "sqlite3"
end end

View File

@ -1,57 +1,83 @@
require "mail" require "mail"
require "mini-smtp-server"
class ReplyEmailSmtpServer < MiniSmtpServer
def new_message_event(message_hash)
m = /<(?<recipient>[^<>]+)>/.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 namespace :foodsoft do
desc "Parse incoming email on stdin (options: RECIPIENT=f.1.2.a1b2c3d3e5)" desc "Parse incoming email on stdin (options: RECIPIENT=f.1.2.a1b2c3d3e5)"
task :parse_reply_email => :environment do task :parse_reply_email => :environment do
m = /(?<foodcoop>[^@]*)\.(?<message_id>\d+)\.(?<user_id>\d+)\.(?<hash>\w+)(@(?<hostname>.*))?/.match(ENV['RECIPIENT']) hande_mail(ENV['RECIPIENT'], STDIN.read)
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
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 = /(?<foodcoop>[^@]*)\.(?<message_id>\d+)\.(?<user_id>\d+)\.(?<hash>\w+)(@(?<hostname>.*))?/.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 end
# Helper # Helper