diff --git a/.gitignore b/.gitignore index 604aed868..a9d70e805 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ config/database.yml config/mongoid.yml config/config.yml config/redis.yml +config/mailman.yml config/mailer_daemon.yml public/uploads/**/* public/topics diff --git a/Gemfile b/Gemfile index 9f25a0fb4..b8982c83e 100644 --- a/Gemfile +++ b/Gemfile @@ -54,7 +54,6 @@ gem 'mail_view', :git => 'git://github.com/37signals/mail_view.git' gem "daemon-spawn", "~> 0.4.2" gem "unicorn" gem "rb-inotify" -gem "mailman" # 用于组合小图片 gem "sprite-factory", "1.4.1" @@ -68,6 +67,14 @@ group :assets do gem 'uglifier' end +group :mailman do + gem "rb-inotify" + gem "mailman" + gem "nokogiri", "1.5.0" + gem "daemon-spawn", "~> 0.4.2" + gem "resque", "~> 1.19.0", :require => "resque/server" +end + group :development do gem 'capistrano', '2.9.0' gem 'chunky_png', "1.2.5" diff --git a/README.markdown b/README.markdown index 43760a93d..f09cab61d 100644 --- a/README.markdown +++ b/README.markdown @@ -9,11 +9,13 @@ This is source code of [Ruby China Group](http://ruby-china.org) cp config/config.yml.default config/config.yml cp config/mongoid.yml.default config/mongoid.yml cp config/redis.yml.default config/redis.yml + cp config/mailman.yml.default config/mailman.yml bundle install bundle update rails rake assets:precompile thin start -O -C config/thin.yml ./script/resque start + ./script/mailman start easy_install pygments # 或者 pip install pygments ``` @@ -53,6 +55,12 @@ This is source code of [Ruby China Group](http://ruby-china.org) Dalli requires memcached 1.4.x + +## Mailman + +如要啟動電郵回覆功能,請啟動 ./script/mailman + +要設定 ./config/mailman.yml 到適當的 pop3 電郵,如果使用 gmail ,請確認已啟動 pop3接收郵件功能。 + ## Helpers render_topic_title(topic) diff --git a/app/helpers/mail_helper.rb b/app/helpers/mail_helper.rb new file mode 100644 index 000000000..2847e9ca5 --- /dev/null +++ b/app/helpers/mail_helper.rb @@ -0,0 +1,35 @@ +require 'nokogiri' + +module MailHelper + + # find the email body in a message + def plaintext_body(message) + if message.multipart? + message.parts.each do |p| + if p.content_type =~ /text\/plain/ + encoding = p.content_type.to_s.split("=").last.to_s + return extract_content(p.body,encoding) + end + end + raise "mail body multipart, but not text/plain part" + elsif message.content_type == 'text/plain' + return extract_content(message.body, message.encoding) + else + raise "mail body is not multipart nor text/plain" + end + end + + # extract email content from a body + # use the sender email line as separation + def extract_reply(body, sender_email) + body.strip + .gsub(/\n^[^\r\n]*#{sender_email}.*:.*\z/m, '') + .strip + end + + private + def extract_content(html, coding) + doc = Nokogiri::HTML(html.to_s, nil, coding) + return doc.css("body").text + end +end diff --git a/app/models/reply_listener.rb b/app/models/reply_listener.rb index 583240729..b95de7059 100644 --- a/app/models/reply_listener.rb +++ b/app/models/reply_listener.rb @@ -1,18 +1,22 @@ # reply email and create topic as needed class ReplyListener - def self.perform(reply_id, key, from, text, message_id) + @queue = :mailer + + def self.perform(reply_id, key, message_id, from_email, reply_text) previous_reply = Reply.find(reply_id) - recipient = User.find(from) + reply_user = User.find_by_email(from_email) - valid_reply_key = TopicMailer.reply_key(previous_reply.email_key, recipient.email) + valid_reply_key = TopicMailer.reply_key(previous_reply.email_key, reply_user.email) if valid_reply_key != key raise "Invalid reply: #{reply_id}, key: #{key}, from: #{from}" end - reply = previous_reply.topic.replies.build - reply.body = text + reply = Reply.new + reply.topic_id = previous_reply.topic_id reply.user_id = reply_user.id + reply.body = reply_text + reply.source = 'mail' reply.message_id = message_id reply.save! end diff --git a/config/initializers/redis.rb b/config/initializers/redis.rb index dd607d6c5..135e23713 100644 --- a/config/initializers/redis.rb +++ b/config/initializers/redis.rb @@ -19,5 +19,5 @@ require "topic" Resque::Mailer.default_queue_name = "mailer" -Resque.redis = Redis.new(:host => redis_config['host'],:port => redis_config['port']) -Resque.redis.namespace = "resque:ruby-taiwan" \ No newline at end of file +Resque.redis = Redis.new(:host => redis_config['host'], :port => redis_config['port']) +Resque.redis.namespace = redis_config['redis_namespace'] \ No newline at end of file diff --git a/config/mailman.yml.default b/config/mailman.yml.default new file mode 100644 index 000000000..1746e90f2 --- /dev/null +++ b/config/mailman.yml.default @@ -0,0 +1,20 @@ +defaults: &defaults + server: 'pop.gmail.com' + port: 995 + ssl: true + +development: + <<: *defaults + username: '' + password: '' + +test: + <<: *defaults + username: '' + password: '' + +production: + <<: *defaults + username: '' + password: '' + diff --git a/script/mailman b/script/mailman new file mode 100755 index 000000000..fdd99456c --- /dev/null +++ b/script/mailman @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby +require 'rubygems' +require 'bundler' +Bundler.require :mailman + +require File.expand_path('../../app/helpers/mail_helper', __FILE__) +require File.expand_path('../../app/models/reply_listener', __FILE__) +include MailHelper + +require 'daemon_spawn' + +env = ENV['RAILS_ENV'] || 'development' +config = YAML::load(open(File.expand_path('../../config/mailman.yml', __FILE__)))[env] +Mailman.config.pop3 = { + :username => config["username"], + :password => config["password"], + :server => config["server"], + :port => config["port"], + :ssl => config["ssl"] +} + +redis_config = YAML.load_file(File.expand_path('../../config/redis.yml', __FILE__))[env] +Resque.redis = Redis.new(:host => redis_config['host'], :port => redis_config['port']) +Resque.redis.namespace = redis_config['redis_namespace'] + +working_dir = File.expand_path('../../', __FILE__) +logs = "#{working_dir}/log/mailman.log" +pids = "#{working_dir}/tmp/pids/mailman.pid" + +class MailmanDaemon < DaemonSpawn::Base + def start(args) + Mailman::Application.run do + to('notification+%reply_id%-%key%@ruby-hk.org') do |reply_id, key| + begin + from_email = message.from.first + message_id = message.message_id + reply_text = extract_reply(plaintext_body(message), Setting.email_sender) + puts "receive email from #{from_email}, reply_id: #{reply_id}, key: #{key}" + + Resque.enqueue(ReplyListener, reply_id, key, message_id, from_email, reply_text) + rescue StandardError => e + puts "ERROR: #{e.inspect}" + + # write mail to a file for debug + emailfile = "#{working_dir}/log/mailman-#{rand(100000)}.eml" + puts "Write file to debug: #{emailfile}" + File.open(emailfile, 'w') {|f| f.write(message) } + end + end + end + end + + def stop + puts "Try to shutdown mailman" + end +end + + +MailmanDaemon.spawn!({ + :processes => 1, + :log_file => logs, + :pid_file => pids, + :sync_log => true, + :working_dir => working_dir, + :singleton => true +}) + + + diff --git a/script/mailman.rb b/script/mailman.rb deleted file mode 100644 index 692e7f0e8..000000000 --- a/script/mailman.rb +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env ruby -require 'rubygems' -require 'mailman' -require 'resque' - -env = ENV['RAILS_ENV'] || 'development' -config = YAML::load(open("./config/mailer_daemon.yml"))[env] -Mailman.config.pop3 = { - :username => config["username"], - :password => config["password"], - :server => config["server"], - :port => config["port"], - :ssl => config["ssl"] -} - -def plaintext_body(message) - if message.multipart? - message.parts.each do |p| - if p.content_type =~ /text\/plain/ - encoding = p.content_type.to_s.split("=").last.to_s - return safe_str_encoding(p.body,encoding) - end - end - raise "mail body multipart, but not text/plain part" - elsif message.content_type == 'text/plain' - return safe_str_encoding(message.body, message.encoding) - else - raise "mail body is not multipart nor text/plain" - end -end - -def safe_str_encoding(html, coding) - doc = Nokogiri::HTML(html.to_s, nil, coding) - return doc.css("body").text -end - -Mailman::Application.run do - to('notification+%reply_id%-%key%@ruby-hk.org') do |reply_id, key| - puts "from: #{message.from.first}" - puts "reply_id: #{reply_id}" - puts "key: #{key}" - puts "message_id: #{message.message_id}" - puts "body: #{plaintext_body(message)}" - - end -end