2013-06-25 20:58:52 +02:00
|
|
|
#!/usr/bin/env ruby
|
|
|
|
#
|
|
|
|
# postfix main.cf:
|
2013-06-30 08:29:51 +02:00
|
|
|
# relay_domains = example.com
|
2013-06-25 20:58:52 +02:00
|
|
|
# transport_maps = hash:/etc/postfix/transport
|
|
|
|
# defer_transports =
|
|
|
|
#
|
|
|
|
# postfix transports:
|
2013-06-30 08:29:51 +02:00
|
|
|
# example.com lobsters:
|
2013-06-25 20:58:52 +02:00
|
|
|
#
|
|
|
|
# postfix master.cf:
|
|
|
|
# lobsters unix - n n - 2 pipe
|
|
|
|
# flags=Fqhu user=lobsters size=1024000 argv=/d/rails/lobsters/script/parse_inbound_mail ${recipient} ${sender}
|
|
|
|
|
|
|
|
ENV["RAILS_ENV"] ||= "production"
|
|
|
|
|
|
|
|
APP_PATH = File.expand_path('../../config/application', __FILE__)
|
|
|
|
require File.expand_path('../../config/boot', __FILE__)
|
|
|
|
require APP_PATH
|
|
|
|
Rails.application.require_environment!
|
|
|
|
|
|
|
|
# postfix exit codes
|
|
|
|
EX_NOUSER = 67
|
|
|
|
EX_TEMPFAIL = 75
|
|
|
|
EX_UNAVAILABLE = 69
|
|
|
|
|
|
|
|
recipient = ARGV[0]
|
2013-06-30 08:29:51 +02:00
|
|
|
user_token = recipient.gsub(/^#{Rails.application.shortname}-/, "").
|
|
|
|
gsub(/@.*/, "")
|
2013-06-25 20:58:52 +02:00
|
|
|
sender = ARGV[1]
|
|
|
|
message = ""
|
|
|
|
email = nil
|
|
|
|
|
|
|
|
while !STDIN.eof?
|
|
|
|
message += STDIN.gets.to_s
|
|
|
|
end
|
|
|
|
|
2013-06-30 08:29:51 +02:00
|
|
|
if message.match(/^X-BeenThere: #{Rails.application.shortname}-/i)
|
2013-06-25 20:58:52 +02:00
|
|
|
# avoid looping
|
|
|
|
exit
|
|
|
|
end
|
|
|
|
|
|
|
|
sending_user = User.where(:mailing_list_enabled => true,
|
|
|
|
:mailing_list_token => user_token).first
|
|
|
|
|
|
|
|
if !sending_user
|
|
|
|
STDERR.puts "no user with mailing list token #{user_token}"
|
|
|
|
|
|
|
|
# if this looks like a user token but invalid, generate a bounce to be
|
|
|
|
# helpful. otherwise supress it to avoid talking back to spammers
|
2013-06-30 08:29:51 +02:00
|
|
|
exit(recipient.match(/^#{Rails.application.shortname}-/) ? EX_NOUSER : 0)
|
2013-06-25 20:58:52 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# the mail gem stupidly spams STDERR while parsing e-mail, so silence that
|
|
|
|
# stream to avoid anything getting back to postfix
|
|
|
|
begin
|
|
|
|
Utils.silence_stream(STDERR) do
|
|
|
|
email = Mail.read_from_string(message)
|
|
|
|
end
|
|
|
|
|
|
|
|
if !email
|
|
|
|
raise
|
|
|
|
end
|
|
|
|
rescue
|
|
|
|
STDERR.puts "error parsing e-mail"
|
|
|
|
exit EX_UNAVAILABLE
|
|
|
|
end
|
|
|
|
|
|
|
|
# figure out what this reply is to
|
|
|
|
irt = email[:in_reply_to].to_s.gsub(/[^A-Za-z0-9@\.]/, "")
|
|
|
|
|
|
|
|
if m = irt.match(/^comment\.([^\.]+)\.\d+@/)
|
|
|
|
parent = Comment.find_by_short_id(m[1])
|
|
|
|
elsif m = irt.match(/^story\.([^\.]+)\.\d+@/)
|
|
|
|
parent = Story.find_by_short_id(m[1])
|
|
|
|
end
|
|
|
|
|
|
|
|
if !parent
|
|
|
|
STDERR.puts "no valid comment or story being replied to"
|
|
|
|
exit EX_NOUSER
|
|
|
|
end
|
|
|
|
|
|
|
|
body = nil
|
|
|
|
possible_charset = nil
|
|
|
|
|
|
|
|
if email.multipart?
|
|
|
|
# parts[0] - multipart/alternative
|
|
|
|
# parts[0].parts[0] - text/plain
|
|
|
|
# parts[0].parts[1] - text/html
|
|
|
|
if (p = email.parts.first.parts.select{|p|
|
|
|
|
p.content_type.match(/text\/plain/) }).any?
|
|
|
|
begin
|
|
|
|
possible_charset = p.first.content_type_parameters["charset"]
|
|
|
|
rescue
|
|
|
|
end
|
|
|
|
|
|
|
|
# parts[0] - text/plain
|
|
|
|
elsif (p = email.parts.select{|p|
|
|
|
|
p.content_type.match(/text\/plain/) }).any?
|
|
|
|
body = p.first.body.to_s
|
|
|
|
|
|
|
|
begin
|
|
|
|
possible_charset = p.first.content_type_parameters["charset"]
|
|
|
|
rescue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
elsif email.content_type.to_s.match(/text\/plain/)
|
|
|
|
body = email.body.to_s
|
|
|
|
|
|
|
|
begin
|
|
|
|
possible_charset = email.content_type_parameters["charset"]
|
|
|
|
rescue
|
|
|
|
end
|
|
|
|
|
|
|
|
elsif !email.content_type.to_s.present?
|
|
|
|
# no content-type header, assume it's text/plain
|
|
|
|
body = email.body.to_s
|
|
|
|
end
|
|
|
|
|
|
|
|
if !body.present?
|
|
|
|
# oh well
|
|
|
|
STDERR.puts "no valid text/plain body found"
|
|
|
|
exit EX_UNAVAILABLE
|
|
|
|
end
|
|
|
|
|
|
|
|
# try to remove sig lines
|
|
|
|
body.gsub!(/^-- \n.+\z/, "")
|
|
|
|
|
|
|
|
# TODO: try to strip out attribution line, followed by an optional blank line,
|
|
|
|
# and then lines prefixed with >
|
|
|
|
|
|
|
|
body.strip!
|
|
|
|
|
|
|
|
c = Comment.new
|
|
|
|
c.user_id = sending_user.id
|
|
|
|
c.comment = body
|
|
|
|
|
|
|
|
if parent.is_a?(Comment)
|
|
|
|
c.story_id = parent.story_id
|
|
|
|
c.parent_comment_id = parent.id
|
|
|
|
else
|
|
|
|
c.story_id = parent.id
|
|
|
|
end
|
|
|
|
|
|
|
|
if c.save
|
|
|
|
exit
|
|
|
|
else
|
|
|
|
STDERR.puts c.errors.inspect
|
|
|
|
exit EX_UNAVAILABLE
|
|
|
|
end
|