#!/usr/bin/env ruby # # postfix main.cf: # relay_domains = example.com # transport_maps = hash:/etc/postfix/transport # defer_transports = # # postfix transports: # example.com lobsters: # # 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] user_token = recipient.gsub(/^#{Rails.application.shortname}-/, ""). gsub(/@.*/, "") sender = ARGV[1] message = "" email = nil while !STDIN.eof? message += STDIN.gets.to_s end if message.match(/^X-BeenThere: #{Rails.application.shortname}-/i) # 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 exit(recipient.match(/^#{Rails.application.shortname}-/) ? EX_NOUSER : 0) 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.where(:short_id => m[1]).first elsif m = irt.match(/^story\.([^\.]+)\.\d+@/) parent = Story.where(:short_id => m[1]).first 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 c.is_from_email = true 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