├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── bin └── gitlab-mail-receiver ├── gitlab-mail-receiver.gemspec ├── lib ├── gitlab-mail-receiver.rb └── mail-receiver │ ├── body_parser.rb │ ├── cli.rb │ ├── daemon.rb │ ├── encoder.rb │ ├── receiver.rb │ ├── reply_to.rb │ └── version.rb └── spec ├── body_parser_spec.rb ├── encoder_spec.rb ├── receiver_spec.rb ├── reply_to_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | capybara-*.html 3 | .rspec 4 | /log 5 | /tmp 6 | /db/*.sqlite3 7 | /db/*.sqlite3-journal 8 | /public/system 9 | /coverage/ 10 | /spec/tmp 11 | **.orig 12 | rerun.txt 13 | pickle-email-*.html 14 | *.gem 15 | 16 | # TODO Comment out these rules if you are OK with secrets being uploaded to the repo 17 | config/initializers/secret_token.rb 18 | config/secrets.yml 19 | 20 | ## Environment normalisation: 21 | /.bundle 22 | /vendor/bundle 23 | 24 | # these should all be checked in to normalise the environment: 25 | # Gemfile.lock, .ruby-version, .ruby-gemset 26 | 27 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 28 | .rvmrc 29 | 30 | # if using bower-rails ignore default bower_components path bower.json files 31 | /vendor/assets/bower_components 32 | *.bowerrc 33 | bower.json 34 | 35 | # Ignore pow environment settings 36 | .powenv 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | 2.1 5 | 6 | script: bundle exec rspec spec 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.1 2 | 3 | - Fix bug, Commit Email can't work, because its not have iid method, skip it. 4 | 5 | ## 0.1.0 6 | 7 | - User GitHub email_reply_parser gem to parse the mail body. 8 | 9 | ## 0.0.4 10 | 11 | - Fix Mail body parser bug, the previous version will lose the content after the newline. 12 | - Body parse don't covert chars, to fix Chinese charset encoding bug. 13 | - Fix the encoding error, some mail receive content encoding not UTF-8, force convert it. 14 | 15 | ## 0.0.3 16 | 17 | - Fixed the MergeRequest can't create Note with reply email bug, missing the `t` field in the `reply_to` address. 18 | 19 | ## 0.0.2 20 | 21 | - Add Daemon feature. 22 | - Change Reply email address to QueryString style, for example: `foo+p=foo/bar&id=123&n=43&t=m@gitlab.com`; 23 | - Add Reply target support, this will fix relationship of the replies; 24 | - Add test case; 25 | 26 | ## 0.0.1 27 | 28 | First release. 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | gemspec 5 | 6 | gem 'rspec' 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gitlab-mail-receiver (0.1.1) 5 | activesupport (>= 4.0) 6 | email_reply_parser (~> 0.5.8) 7 | mailman (~> 0.7.3) 8 | rack (>= 1.0) 9 | thor (>= 0.17.0) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | activesupport (4.2.4) 15 | i18n (~> 0.7) 16 | json (~> 1.7, >= 1.7.7) 17 | minitest (~> 5.1) 18 | thread_safe (~> 0.3, >= 0.3.4) 19 | tzinfo (~> 1.1) 20 | celluloid (0.16.0) 21 | timers (~> 4.0.0) 22 | diff-lcs (1.2.5) 23 | email_reply_parser (0.5.8) 24 | ffi (1.9.10) 25 | hitimes (1.2.3) 26 | i18n (0.7.0) 27 | json (1.8.3) 28 | listen (2.10.1) 29 | celluloid (~> 0.16.0) 30 | rb-fsevent (>= 0.9.3) 31 | rb-inotify (>= 0.9) 32 | mail (2.6.3) 33 | mime-types (>= 1.16, < 3) 34 | maildir (2.2.1) 35 | mailman (0.7.3) 36 | activesupport (>= 2.3.4) 37 | i18n (>= 0.4.1) 38 | listen (~> 2.2) 39 | mail (>= 2.0.3) 40 | maildir (>= 0.5.0) 41 | mime-types (2.6.2) 42 | minitest (5.8.1) 43 | rack (1.6.4) 44 | rb-fsevent (0.9.6) 45 | rb-inotify (0.9.5) 46 | ffi (>= 0.5.0) 47 | rspec (3.3.0) 48 | rspec-core (~> 3.3.0) 49 | rspec-expectations (~> 3.3.0) 50 | rspec-mocks (~> 3.3.0) 51 | rspec-core (3.3.2) 52 | rspec-support (~> 3.3.0) 53 | rspec-expectations (3.3.1) 54 | diff-lcs (>= 1.2.0, < 2.0) 55 | rspec-support (~> 3.3.0) 56 | rspec-mocks (3.3.2) 57 | diff-lcs (>= 1.2.0, < 2.0) 58 | rspec-support (~> 3.3.0) 59 | rspec-support (3.3.0) 60 | thor (0.19.1) 61 | thread_safe (0.3.5) 62 | timers (4.0.4) 63 | hitimes 64 | tzinfo (1.2.2) 65 | thread_safe (~> 0.1) 66 | 67 | PLATFORMS 68 | ruby 69 | 70 | DEPENDENCIES 71 | gitlab-mail-receiver! 72 | rspec 73 | 74 | BUNDLED WITH 75 | 1.10.6 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Li Huashun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitLab Mail Receiver 2 | 3 | [![Gem Version](https://badge.fury.io/rb/gitlab-mail-receiver.svg)](http://badge.fury.io/rb/gitlab-mail-receiver) [![CI Status](https://travis-ci.org/huacnlee/gitlab-mail-receiver.svg)](https://travis-ci.org/huacnlee/gitlab-mail-receiver) 4 | 5 | The way of allow your GitLab support Email receive and parse the email content, and find Issue/MergeRequest to create reply. 6 | 7 | [中文介绍](https://ruby-china.org/topics/27143) 8 | 9 | ------------ 10 | 11 | > PS: After my created this gem, GitLab have released 8.0, and it included same feature. So you don't need this. 12 | 13 | ## Features 14 | 15 | - Receive Email reply to check Issue/MergeRequest and create comment. 16 | - Very easy to configure on GitLab project. 17 | - Cleanup the mail content. 18 | 19 | The WorkFlow: 20 | 21 | ``` 22 | /--> [ Notify ] ----------------> [Mail Server] <---> [Mail Client] 23 | { GitLab } ---/ ^ 24 | ^ | 25 | |-------< [ gitlab-mail-receiver ] <---- check --> | 26 | ``` 27 | 28 | ## Requirements 29 | 30 | - GitLab 7.13 (I just tested on this version.) 31 | - An Email can receive mail via POP3/IMAP. 32 | 33 | 34 | ## Configuration 35 | 36 | Add this gem in GitLab project Gemfile: 37 | 38 | ```rb 39 | gem 'gitlab-mail-receiver' 40 | ``` 41 | 42 | Create initialize file in GitLab `config/initializes/gitlab-mail-receiver.rb`: 43 | 44 | ```rb 45 | require 'gitlab-mail-receiver' 46 | 47 | Notify.send(:prepend, MailReceiver::ReplyTo) 48 | 49 | MailReceiver.configure do 50 | self.sender = 'xxx@your-mail-host.com' 51 | self.poll_interval = 5 52 | self.imap = { 53 | server: 'imap.your-mail-host.com', 54 | port: 993, 55 | ssl: true, 56 | username: 'xxx@your-mail-host.com', 57 | password: 'your-password' 58 | } 59 | end 60 | ``` 61 | 62 | ## Run commands 63 | 64 | ``` 65 | $ cd gitlab 66 | $ bundle exec gitlab-mail-receiver -h 67 | Commands: 68 | gitlab-mail-receiver help [COMMAND] # Describe available commands or one specific command 69 | gitlab-mail-receiver restart # Restart Daemon 70 | gitlab-mail-receiver start # Start Daemon 71 | gitlab-mail-receiver stop # Stop Daemon 72 | gitlab-mail-receiver version # Show version 73 | 74 | Options: 75 | [--root=ROOT] 76 | # Default: ./ 77 | $ bundle exec gitlab-mail-receiver start 78 | Started gitlab-mail-receiver on pid: 59386 79 | I, [2015-09-01T13:36:50.813124 #59387] INFO -- : Celluloid 0.17.1.2 is running in BACKPORTED mode. [ http://git.io/vJf3J ] 80 | ... 81 | ``` 82 | 83 | ## Run in production 84 | 85 | ``` 86 | $ cd gitlab 87 | $ RAILS_ENV=production bundle exec gitlab-mail-receiver start -d 88 | pid_file: ./tmp/pids/gitlab-mail-receiver.pid 89 | log_file: ./log/gitlab-mail-receiver.log 90 | Started gitlab-mail-receiver on pid: 58861 91 | ``` 92 | 93 | > NOTE: The daemon log will write to `$rails_root/log/gitlab-mail-receiver.log` 94 | 95 | Stop daemon 96 | 97 | ```bash 98 | $ bundle exec gitlab-mail-receiver stop 99 | Stoping gitlab-mail-receiver... [OK] 100 | ``` 101 | 102 | 103 | ### Daemon Signals 104 | 105 | gitlab-mail-receiver has support the [Unix process signal](https://en.wikipedia.org/wiki/Unix_signal) to manage the daemon. 106 | 107 | You can use the `kill` command to send the signal to the master process. 108 | 109 | - USR2 - Hot reload processes. 110 | - QUIT - Stop processes. 111 | 112 | ``` 113 | $ ps aux | grep gitlab-mail-receiver 114 | git 15488 0.2 0.2 612612 242920 pts/3 Sl 14:24 0:16 gitlab-mail-receiver [worker] 115 | git 16320 0.0 0.0 309100 43004 pts/3 Sl 11:54 0:00 gitlab-mail-receiver [master] 116 | $ kill -USR2 15488 117 | ``` 118 | 119 | -------------------------------------------------------------------------------- /bin/gitlab-mail-receiver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../lib/mail-receiver/cli' 3 | 4 | MailReceiver::CLI.start(ARGV) 5 | -------------------------------------------------------------------------------- /gitlab-mail-receiver.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/mail-receiver/version', __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "gitlab-mail-receiver" 5 | s.version = MailReceiver.version 6 | s.platform = Gem::Platform::RUBY 7 | s.authors = ["Jason Lee"] 8 | s.email = ["huacnlee@gmail.com"] 9 | s.homepage = "http://github.com/huacnlee/gitlab-mail-receiver" 10 | s.summary = "Allow your GitLab receive mails to create Issue comment" 11 | s.description = "The way of allow your GitLab support Email receive and parse the email content, and find Issue/MergeRequest to create reply." 12 | s.license = 'MIT' 13 | s.required_rubygems_version = ">= 1.3.6" 14 | 15 | s.add_runtime_dependency("mailman", '~> 0.7.3') 16 | s.add_runtime_dependency("activesupport", ">= 4.0") 17 | s.add_runtime_dependency("rack", '>= 1.0') 18 | s.add_runtime_dependency("thor", ">= 0.17.0") 19 | s.add_runtime_dependency("email_reply_parser", "~> 0.5.8") 20 | 21 | s.bindir = 'bin' 22 | s.executables << 'gitlab-mail-receiver' 23 | s.files = Dir.glob("lib/**/*") + %w(README.md LICENSE) 24 | s.require_path = 'lib' 25 | end 26 | -------------------------------------------------------------------------------- /lib/gitlab-mail-receiver.rb: -------------------------------------------------------------------------------- 1 | require_relative './mail-receiver/encoder' 2 | require_relative './mail-receiver/body_parser' 3 | require_relative './mail-receiver/receiver' 4 | require_relative './mail-receiver/reply_to' 5 | 6 | require 'mailman' 7 | require 'email_reply_parser' 8 | require 'active_support/core_ext' 9 | 10 | # Extend Mailman config to add sender attribute 11 | module MailmanConfig 12 | extend ActiveSupport::Concern 13 | 14 | included do 15 | attr_accessor :sender 16 | end 17 | end 18 | 19 | Mailman::Configuration.send(:include, MailmanConfig) 20 | 21 | # MailReceiver 22 | module MailReceiver 23 | def self.config 24 | Mailman.config 25 | end 26 | 27 | def self.configure(&block) 28 | Mailman.config.instance_exec(&block) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/mail-receiver/body_parser.rb: -------------------------------------------------------------------------------- 1 | module MailReceiver 2 | # Mail body context parser 3 | module BodyParser 4 | def extract 5 | EmailReplyParser.read(part.to_s) 6 | .fragments.map(&:to_s) 7 | .join("\n").rstrip 8 | .force_encoding('utf-8') 9 | end 10 | 11 | def part 12 | mail.multipart? ? mail.parts.first.body : mail.body 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mail-receiver/cli.rb: -------------------------------------------------------------------------------- 1 | require_relative './daemon' 2 | require 'thor' 3 | 4 | module MailReceiver 5 | class CLI < Thor 6 | include Thor::Actions 7 | 8 | map '-v' => :version 9 | map "s" => :start 10 | class_option :root, type: :string, default: './' 11 | 12 | option :daemon, type: :boolean, aliases: ['d'], default: false 13 | desc "start", "Start Daemon" 14 | def start 15 | MailReceiver::Daemon.init(options) do 16 | begin 17 | rails_env = ::File.expand_path('./config/environment', options[:root]) 18 | require rails_env 19 | rescue => e 20 | puts "You need run this command under GitLab root." 21 | return 22 | end 23 | 24 | Mailman.config.logger = Logger.new($stdout) 25 | Mailman.config.rails_root = options[:root] 26 | 27 | Mailman.config.logger.info "Starting gitlab-mail-receiver..." 28 | Mailman::Application.run do 29 | to '%user%+%suffix%@%host%' do 30 | @receiver = MailReceiver::Receiver.new(message, logger: Mailman.config.logger) 31 | @receiver.process! 32 | end 33 | end 34 | end 35 | MailReceiver::Daemon.start_process 36 | end 37 | 38 | desc "stop", "Stop Daemon" 39 | def stop 40 | MailReceiver::Daemon.init(options) 41 | MailReceiver::Daemon.stop_process 42 | end 43 | 44 | desc "restart", "Restart Daemon" 45 | def restart 46 | MailReceiver::Daemon.init(options) 47 | MailReceiver::Daemon.restart_process 48 | end 49 | 50 | desc "version", "Show version" 51 | def version 52 | puts "gitlab-mail-receiver #{MailReceiver.version}" 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/mail-receiver/daemon.rb: -------------------------------------------------------------------------------- 1 | # Daemon code from: https://github.com/huacnlee/sails 2 | module MailReceiver 3 | class Daemon 4 | class << self 5 | attr_accessor :options, :daemon, :mode, :app_name, :pid_file, :log_file, :runblock 6 | 7 | def init(opts = {}, &block) 8 | self.app_name = 'gitlab-mail-receiver' 9 | self.pid_file = File.join(opts[:root], "tmp/pids/gitlab-mail-receiver.pid") 10 | self.log_file = File.join(opts[:root], 'log/gitlab-mail-receiver.log') 11 | self.daemon = opts[:daemon] 12 | self.options = opts 13 | self.runblock = block 14 | end 15 | 16 | def read_pid 17 | if !File.exist?(pid_file) 18 | return nil 19 | end 20 | 21 | pid = File.open(pid_file).read.to_i 22 | begin 23 | Process.getpgid(pid) 24 | rescue 25 | pid = nil 26 | end 27 | pid 28 | end 29 | 30 | def start_process 31 | old_pid = read_pid 32 | if !old_pid.nil? 33 | puts colorize("Current have #{app_name} process in running on pid #{old_pid}", :red) 34 | return 35 | end 36 | 37 | # start master process 38 | @master_pid = fork_master_process! 39 | File.open(pid_file, "w+") do |f| 40 | f.puts @master_pid 41 | end 42 | 43 | if self.daemon 44 | puts "pid_file: #{self.pid_file}" 45 | puts "log_file: #{self.log_file}" 46 | end 47 | puts "Started #{app_name} on pid: #{@master_pid}" 48 | # puts "in init: #{Sails.service.object_id}" 49 | 50 | if not self.daemon 51 | Process.waitpid(@master_pid) 52 | else 53 | exit 54 | end 55 | end 56 | 57 | def restart_process(options = {}) 58 | old_pid = read_pid 59 | if old_pid == nil 60 | puts colorize("#{app_name} process not found on pid #{old_pid}", :red) 61 | return 62 | end 63 | 64 | print "Restarting #{app_name}..." 65 | Process.kill("USR2", old_pid) 66 | puts colorize(" [OK]", :green) 67 | end 68 | 69 | def fork_master_process! 70 | fork do 71 | # WARN: DO NOT CALL Sails IN THIS BLOCK! 72 | $PROGRAM_NAME = self.app_name + " [master]" 73 | @child_pid = fork_child_process! 74 | 75 | Signal.trap("QUIT") do 76 | Process.kill("QUIT", @child_pid) 77 | exit 78 | end 79 | 80 | Signal.trap("USR2") do 81 | Process.kill("USR2", @child_pid) 82 | end 83 | 84 | loop do 85 | sleep 1 86 | begin 87 | Process.getpgid(@child_pid) 88 | rescue Errno::ESRCH 89 | @child_pid = fork_child_process! 90 | end 91 | end 92 | end 93 | end 94 | 95 | def fork_child_process! 96 | pid = fork do 97 | $PROGRAM_NAME = self.app_name + " [worker]" 98 | Signal.trap("QUIT") do 99 | exit 100 | end 101 | 102 | Signal.trap("USR2") do 103 | # TODO: reload Sails in current process 104 | exit 105 | end 106 | 107 | if self.daemon == true 108 | redirect_stdout 109 | end 110 | 111 | # puts "in child: #{Sails.service.object_id}" 112 | self.runblock.call 113 | end 114 | # http://ruby-doc.org/core-1.9.3/Process.html#detach-method 115 | Process.detach(pid) 116 | pid 117 | end 118 | 119 | def stop_process 120 | pid = read_pid 121 | if pid.nil? 122 | puts colorize("#{app_name} process not found, pid #{pid}", :red) 123 | return 124 | end 125 | 126 | print "Stoping #{app_name}..." 127 | begin 128 | Process.kill("QUIT", pid) 129 | ensure 130 | File.delete(pid_file) 131 | end 132 | puts colorize(" [OK]", :green) 133 | end 134 | 135 | private 136 | # Redirect stdout, stderr to log file, 137 | # If we not do this, stdout will block sails daemon, for example `puts`. 138 | def redirect_stdout 139 | redirect_io($stdout, self.log_file) 140 | redirect_io($stderr, self.log_file) 141 | end 142 | 143 | def redirect_io(io, path) 144 | File.open(path, 'ab') { |fp| io.reopen(fp) } if path 145 | io.sync = true 146 | end 147 | 148 | def colorize(text, c) 149 | case c 150 | when :red 151 | return ["\033[31m",text,"\033[0m"].join("") 152 | when :green 153 | return ["\033[32m",text,"\033[0m"].join("") 154 | when :blue 155 | return ["\033[34m",text,"\033[0m"].join("") 156 | else 157 | return text 158 | end 159 | end 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/mail-receiver/encoder.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/hash' 2 | require 'rack' 3 | 4 | module MailReceiver 5 | module Encoder 6 | class << self 7 | def encode(hash) 8 | hash.to_query 9 | end 10 | 11 | def decode(query) 12 | Rack::Utils.parse_query(query).deep_symbolize_keys 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mail-receiver/receiver.rb: -------------------------------------------------------------------------------- 1 | module MailReceiver 2 | class Receiver 3 | include BodyParser 4 | 5 | def initialize(message, opts = {}) 6 | @message = message 7 | @logger = opts[:logger] 8 | end 9 | 10 | def mail 11 | @message 12 | end 13 | 14 | def project_slug 15 | hash_data[:p] 16 | end 17 | 18 | def issue_id 19 | hash_data[:id] 20 | end 21 | 22 | def target_id 23 | hash_data[:n] 24 | end 25 | 26 | def merge_request? 27 | @merge_request ||= (hash_data[:t] || 'i').downcase == 'm' 28 | end 29 | 30 | def body 31 | return @body if defined?(@body) 32 | @body = self.extract 33 | @body 34 | end 35 | 36 | def to 37 | @to ||= @message.to.is_a?(Array) ? @message.to.first : @message.to 38 | end 39 | 40 | def from 41 | @from ||= @message.from.is_a?(Array) ? @message.from.first : @message.from 42 | end 43 | 44 | # foo@gmail.com => foo 45 | def prefix 46 | @prefix ||= to.split('@').first 47 | end 48 | 49 | # foo+p=chair/chair&id=123 => { p: chair/chair, id: 123 } 50 | def hash_data 51 | return @hash_data if defined?(@hash_data) 52 | return {} if not prefix.include?('+') 53 | @hash_data = Encoder.decode(prefix.split('+').last) 54 | return @hash_data 55 | end 56 | 57 | def inspect 58 | { project_slug: project_slug, issue_id: issue_id, target_id: target_id, merge_request: merge_request?, to: to, body: body} 59 | end 60 | 61 | def project 62 | @project ||= Project.find_with_namespace(project_slug) 63 | rescue => e 64 | logger.warn "Project: #{project_slug} record not found." 65 | nil 66 | end 67 | 68 | def process! 69 | if current_user.blank? 70 | logger.warn "Reply user: #{from} not found user in GitLab." 71 | return 72 | end 73 | 74 | if project.blank? 75 | logger.warn "Project #{project_slug} is not found." 76 | return 77 | end 78 | 79 | note_params = merge_request? ? process_mr! : process_issue! 80 | return if note_params.blank? 81 | 82 | note_params[:project_id] = project.id 83 | 84 | # relation to target Note 85 | if target_id 86 | target_note = project.notes.find_by_id(target_id) 87 | if target_note 88 | note_params[:commit_id] = target_note.commit_id 89 | note_params[:line_code] = target_note.line_code 90 | end 91 | end 92 | 93 | note_params[:note] = body 94 | 95 | @note = Notes::CreateService.new(project, current_user, note_params).execute 96 | logger.info "Note #{@note.id} created." 97 | end 98 | 99 | def process_mr! 100 | @mr = project.merge_requests.find_by(iid: self.issue_id) 101 | if @mr.blank? 102 | logger.warn "MergeRequest #{self.issue_id} not found." 103 | return nil 104 | end 105 | 106 | logger.info "Found MergeRequest: #{@mr.id}" 107 | { noteable_type: 'MergeRequest', noteable_id: @mr.id } 108 | end 109 | 110 | def process_issue! 111 | @issue = project.issues.find_by(iid: self.issue_id) 112 | if @issue.blank? 113 | logger.warn "Issue #{self.issue_id} not found." 114 | return nil 115 | end 116 | 117 | logger.info "Found issue: #{@issue.id}" 118 | { noteable_type: 'Issue', noteable_id: @issue.id } 119 | end 120 | 121 | def current_user 122 | @current_user ||= User.find_by_any_email(from) 123 | end 124 | 125 | def logger 126 | @logger ||= Logger.new($stdout) 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/mail-receiver/reply_to.rb: -------------------------------------------------------------------------------- 1 | require 'mailman' 2 | require 'json' 3 | 4 | module MailReceiver 5 | module ReplyTo 6 | def mail_new_thread(model, headers = {}, &block) 7 | # Mail receiver 8 | headers[:reply_to] = reply_to_address(model) 9 | 10 | mail(headers, &block) 11 | end 12 | 13 | def mail_answer_thread(model, headers = {}, &block) 14 | if headers[:subject] 15 | headers[:subject].prepend('Re: ') 16 | end 17 | 18 | # Mail receiver 19 | headers[:reply_to] = reply_to_address(model) 20 | 21 | mail(headers, &block) 22 | end 23 | 24 | protected 25 | def reply_to_address(model) 26 | hash = convert_able(model) 27 | return default_email_reply_to if hash.blank? 28 | return default_email_reply_to if @project.blank? 29 | 30 | 31 | hash.merge!({ p: @project.path_with_namespace }) 32 | 33 | suffix = Encoder.encode(hash) 34 | 35 | Mailman.config.sender.sub('@', "+#{suffix}@") 36 | end 37 | 38 | def default_email_reply_to 39 | Gitlab.config.gitlab.email_reply_to 40 | end 41 | 42 | def convert_able(model) 43 | if %W(Issue MergeRequest).index(model.class.name) == -1 44 | return nil 45 | end 46 | 47 | res = { id: model.iid } 48 | if defined?(@note) 49 | # gitlab/app/mailers/emails/notes.rb 里面会声明 @note 50 | res.merge!({ n: @note.id }) 51 | end 52 | 53 | if model.class.name == 'Issue' 54 | res.merge!({ t: 'i' }) 55 | return res 56 | end 57 | 58 | if model.class.name == 'MergeRequest' 59 | res.merge!({ t: 'm' }) 60 | return res 61 | end 62 | 63 | return nil 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/mail-receiver/version.rb: -------------------------------------------------------------------------------- 1 | module MailReceiver 2 | class << self 3 | def version 4 | '0.1.1' 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/body_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'BodyParser' do 4 | class Content < OpenStruct 5 | include MailReceiver::BodyParser 6 | end 7 | 8 | let(:mail) do 9 | super_body = body 10 | Mail::Message.new do 11 | from 'foo@bar.com' 12 | body super_body 13 | subject 'RE: foo 123' 14 | end 15 | end 16 | let(:content) { Content.new(mail: mail) } 17 | 18 | describe '.extract' do 19 | subject { content.extract } 20 | 21 | context 'simple' do 22 | let(:body) { 'Hello world' } 23 | 24 | it { is_expected.to eq body } 25 | 26 | it 'should return utf-8 encoding' do 27 | expect(subject.encoding.to_s).to eq 'UTF-8' 28 | end 29 | end 30 | 31 | context 'multlines' do 32 | let(:body) { "Hello world\n\nThis is last line" } 33 | it { is_expected.to eq body } 34 | end 35 | 36 | context 'Has - prefix & > prefix' do 37 | let(:body) do 38 | %(This is the first line. 39 | 40 | - The line with - prefix 41 | 42 | > 在 2015年9月8日,13:51,xxxx 写道: 43 | > 44 | > "New comment for Issue 1540" GitLab 通知中心,以及 Email 直接回复 Issue 功能 45 | > "Author: huacnlee wrote:" 46 | > 47 | > Hello) 48 | end 49 | 50 | it { is_expected.to eq body } 51 | end 52 | 53 | context 'GBK encoding' do 54 | let(:body) { 'Hello 邮件'.force_encoding('GBK') } 55 | 56 | it 'should work' do 57 | expect(subject).to eq 'Hello 邮件' 58 | expect(subject.encoding.to_s).to eq 'UTF-8' 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/encoder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Encoder' do 4 | let(:t) { '#' } 5 | let(:raw) { { slug: 'huacnlee/gitlab-mail-receiver', t: t, id: "2091" } } 6 | let(:query) { 'id=2091&slug=huacnlee%2Fgitlab-mail-receiver&t=%23' } 7 | 8 | describe '#encode' do 9 | let(:res) { MailReceiver::Encoder.encode(raw) } 10 | 11 | it 'should work' do 12 | expect(res).to eq(query) 13 | end 14 | 15 | context 'has bad char' do 16 | let(:t) { '@+ab' } 17 | 18 | it 'should work' do 19 | expect(res).not_to include('@') 20 | expect(res).not_to include('+') 21 | end 22 | end 23 | end 24 | 25 | describe '#decode' do 26 | it 'should work' do 27 | expect(MailReceiver::Encoder.decode(query)).to eq(raw) 28 | end 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /spec/receiver_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mail/message' 3 | 4 | describe 'Receiver' do 5 | let(:logger) { Logger.new('/dev/null') } 6 | let(:to) { MailReceiver.config.sender } 7 | let(:body) { "hello world" } 8 | let(:message) { 9 | that = self 10 | Mail::Message.new do 11 | from 'foo@bar.com' 12 | to that.to 13 | subject "Test subject" 14 | body that.body 15 | end 16 | } 17 | let(:receiver) { MailReceiver::Receiver.new(message, logger: logger) } 18 | 19 | context 'Bad to address' do 20 | it { expect(receiver.logger).to eq(logger) } 21 | it { expect(receiver.mail).to eq(message) } 22 | it { expect(receiver.hash_data).to eq({}) } 23 | it { expect(receiver.project_slug).to eq(nil) } 24 | it { expect(receiver.project).to eq(nil) } 25 | it { expect(receiver.issue_id).to eq(nil) } 26 | it { expect(receiver.target_id).to eq(nil) } 27 | it { expect(receiver.to).to eq(to) } 28 | it { expect(receiver.from).to eq('foo@bar.com') } 29 | 30 | it "should .body work" do 31 | allow(receiver).to receive(:extract) { 'abc' } 32 | expect(receiver.body).to eq('abc') 33 | end 34 | 35 | it "should .current_user work" do 36 | u = { email: 'foo@bar.com' } 37 | expect(receiver.current_user).to eq(u) 38 | end 39 | end 40 | 41 | context 'normal' do 42 | let(:hash) { { p: 'foo/bar', id: '201', t: 'i', n: '201511' } } 43 | let(:query) { MailReceiver::Encoder.encode(hash) } 44 | let(:to) { "reply+#{query}@gitlab.com" } 45 | 46 | it { expect(receiver.prefix).to eq("reply+#{query}") } 47 | it { expect(receiver.hash_data).to eq(hash) } 48 | it { expect(receiver.project_slug).to eq(hash[:p]) } 49 | it { expect(receiver.project).to eq({ slug: hash[:p] }) } 50 | it { expect(receiver.issue_id).to eq(hash[:id]) } 51 | it { expect(receiver.target_id).to eq(hash[:n]) } 52 | it { expect(receiver.merge_request?).to eq(false) } 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/reply_to_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'ReplyTo' do 4 | class Monkey 5 | include MailReceiver::ReplyTo 6 | 7 | def initialize(opts) 8 | @note = opts[:note] 9 | @project = opts[:project] 10 | end 11 | 12 | def default_email_reply_to 13 | 'foo@bar.com' 14 | end 15 | end 16 | 17 | let(:project) { Project.new(id: 301) } 18 | let(:note) { Note.new(id: 10) } 19 | let(:monkey) { Monkey.new(note: note, project: project) } 20 | 21 | describe '.convert_able' do 22 | context 'Issue' do 23 | let(:obj) { Issue.new(iid: 2) } 24 | 25 | it 'should work' do 26 | res = monkey.send(:convert_able, obj) 27 | expect(res).to eq({ id: 2, n: note.id, t: 'i'}) 28 | end 29 | end 30 | 31 | context 'MergeRequest' do 32 | let(:obj) { MergeRequest.new(iid: 3) } 33 | 34 | it 'should work' do 35 | res = monkey.send(:convert_able, obj) 36 | expect(res).to eq({ id: 3, n: note.id, t: 'm'}) 37 | end 38 | end 39 | 40 | context 'Other' do 41 | let(:obj) { Other.new(iid: 3) } 42 | 43 | it 'should work' do 44 | res = monkey.send(:convert_able, obj) 45 | expect(res).to eq nil 46 | end 47 | end 48 | end 49 | 50 | describe '.reply_to_address' do 51 | context 'Issue' do 52 | let(:obj) { Issue.new(iid: 2) } 53 | 54 | it 'should work' do 55 | res = monkey.send(:reply_to_address, obj) 56 | expect(res).to eq 'reply+id=2&n=10&p=&t=i@gitlab.com' 57 | end 58 | end 59 | 60 | context 'MergeRequest' do 61 | let(:obj) { MergeRequest.new(iid: 4) } 62 | 63 | it 'should work' do 64 | res = monkey.send(:reply_to_address, obj) 65 | expect(res).to eq 'reply+id=4&n=10&p=&t=m@gitlab.com' 66 | end 67 | end 68 | 69 | context 'Other' do 70 | let(:obj) { Other.new(iid: 4) } 71 | 72 | it 'should work' do 73 | res = monkey.send(:reply_to_address, obj) 74 | expect(res).to eq monkey.default_email_reply_to 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'ostruct' 3 | 4 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 5 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 6 | 7 | require "gitlab-mail-receiver" 8 | 9 | # Fake models 10 | class User < OpenStruct 11 | class << self 12 | def find_by_any_email(email) 13 | return nil if email.blank? 14 | return { email: email } 15 | end 16 | end 17 | end 18 | 19 | class Project < OpenStruct 20 | class << self 21 | def find_with_namespace(slug) 22 | raise 'not found project' if slug.blank? 23 | return { slug: slug } 24 | end 25 | end 26 | end 27 | 28 | class Note < OpenStruct; end 29 | class Issue < OpenStruct; end 30 | class MergeRequest < OpenStruct; end 31 | class Other < OpenStruct; end 32 | 33 | MailReceiver.configure do 34 | self.sender = 'reply@gitlab.com' 35 | self.poll_interval = 5 36 | self.imap = { 37 | server: 'imap.gitlab-mail.com', 38 | port: 993, 39 | ssl: true, 40 | username: 'reply@gitlab.com', 41 | password: '123456' 42 | } 43 | end 44 | --------------------------------------------------------------------------------