Commit b3afde14 authored by Jean-Philippe Lang's avatar Jean-Philippe Lang

Ability to accept incoming emails from unknown users (#2230, #3003).

An option lets you specify how to handle emails from unknown users:
* ignore: the email is ignored (previous and default behaviour)
* accept: the sender is considered as an anonymous user
* create: a user account is created (username/password are sent back to the user)

Permissions have to be consistent with the chosen option. Eg. if you choose 'create', the 'Non member' role must have the 'Add issues' permission so that an issue can be created by an unknown user via email. If you choose 'accept', the 'Anonymous' role must have this permission.

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2789 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent c48193f8
...@@ -38,15 +38,34 @@ class MailHandler < ActionMailer::Base ...@@ -38,15 +38,34 @@ class MailHandler < ActionMailer::Base
end end
# Processes incoming emails # Processes incoming emails
# Returns the created object (eg. an issue, a message) or false
def receive(email) def receive(email)
@email = email @email = email
@user = User.active.find_by_mail(email.from.to_a.first.to_s.strip) @user = User.find_by_mail(email.from.to_a.first.to_s.strip)
unless @user if @user && !@user.active?
# Unknown user => the email is ignored logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
# TODO: ability to create the user's account
logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
return false return false
end end
if @user.nil?
# Email was submitted by an unknown user
case @@handler_options[:unknown_user]
when 'accept'
@user = User.anonymous
when 'create'
@user = MailHandler.create_user_from_email(email)
if @user
logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
Mailer.deliver_account_information(@user, @user.password)
else
logger.error "MailHandler: could not create account for [#{email.from.first}]" if logger && logger.error
return false
end
else
# Default behaviour, emails from unknown users are ignored
logger.info "MailHandler: ignoring email from unknown user [#{email.from.first}]" if logger && logger.info
return false
end
end
User.current = @user User.current = @user
dispatch dispatch
end end
...@@ -239,4 +258,23 @@ class MailHandler < ActionMailer::Base ...@@ -239,4 +258,23 @@ class MailHandler < ActionMailer::Base
def self.full_sanitizer def self.full_sanitizer
@full_sanitizer ||= HTML::FullSanitizer.new @full_sanitizer ||= HTML::FullSanitizer.new
end end
# Creates a user account for the +email+ sender
def self.create_user_from_email(email)
addr = email.from_addrs.to_a.first
if addr && !addr.spec.blank?
user = User.new
user.mail = addr.spec
names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
user.firstname = names.shift
user.lastname = names.join(' ')
user.lastname = '-' if user.lastname.blank?
user.login = user.mail
user.password = ActiveSupport::SecureRandom.hex(5)
user.language = Setting.default_language
user.save ? user : nil
end
end
end end
...@@ -15,6 +15,11 @@ ...@@ -15,6 +15,11 @@
# -k, --key Redmine API key # -k, --key Redmine API key
# #
# General options: # General options:
# --unknown-user=ACTION how to handle emails from an unknown user
# ACTION can be one of the following values:
# ignore: email is ignored (default)
# accept: accept as anonymous user
# create: create a user account
# -h, --help show this help # -h, --help show this help
# -v, --verbose show extra information # -v, --verbose show extra information
# -V, --version show version information and exit # -V, --version show version information and exit
...@@ -64,7 +69,7 @@ end ...@@ -64,7 +69,7 @@ end
class RedmineMailHandler class RedmineMailHandler
VERSION = '0.1' VERSION = '0.1'
attr_accessor :verbose, :issue_attributes, :allow_override, :url, :key attr_accessor :verbose, :issue_attributes, :allow_override, :uknown_user, :url, :key
def initialize def initialize
self.issue_attributes = {} self.issue_attributes = {}
...@@ -80,7 +85,8 @@ class RedmineMailHandler ...@@ -80,7 +85,8 @@ class RedmineMailHandler
[ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT], [ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT],
[ '--category', GetoptLong::REQUIRED_ARGUMENT], [ '--category', GetoptLong::REQUIRED_ARGUMENT],
[ '--priority', GetoptLong::REQUIRED_ARGUMENT], [ '--priority', GetoptLong::REQUIRED_ARGUMENT],
[ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT] [ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT],
[ '--unknown-user', GetoptLong::REQUIRED_ARGUMENT]
) )
opts.each do |opt, arg| opts.each do |opt, arg|
...@@ -99,6 +105,8 @@ class RedmineMailHandler ...@@ -99,6 +105,8 @@ class RedmineMailHandler
self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup
when '--allow-override' when '--allow-override'
self.allow_override = arg.dup self.allow_override = arg.dup
when '--unknown-user'
self.unknown_user = arg.dup
end end
end end
...@@ -108,7 +116,9 @@ class RedmineMailHandler ...@@ -108,7 +116,9 @@ class RedmineMailHandler
def submit(email) def submit(email)
uri = url.gsub(%r{/*$}, '') + '/mail_handler' uri = url.gsub(%r{/*$}, '') + '/mail_handler'
data = { 'key' => key, 'email' => email, 'allow_override' => allow_override } data = { 'key' => key, 'email' => email,
'allow_override' => allow_override,
'unknown_user' => unknown_user }
issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value } issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
debug "Posting to #{uri}..." debug "Posting to #{uri}..."
......
...@@ -21,6 +21,13 @@ namespace :redmine do ...@@ -21,6 +21,13 @@ namespace :redmine do
desc <<-END_DESC desc <<-END_DESC
Read an email from standard input. Read an email from standard input.
General options:
unknown_user=ACTION how to handle emails from an unknown user
ACTION can be one of the following values:
ignore: email is ignored (default)
accept: accept as anonymous user
create: create a user account
Issue attributes control options: Issue attributes control options:
project=PROJECT identifier of the target project project=PROJECT identifier of the target project
status=STATUS name of the target status status=STATUS name of the target status
...@@ -47,6 +54,7 @@ END_DESC ...@@ -47,6 +54,7 @@ END_DESC
options = { :issue => {} } options = { :issue => {} }
%w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user']
MailHandler.receive(STDIN.read, options) MailHandler.receive(STDIN.read, options)
end end
...@@ -54,6 +62,13 @@ END_DESC ...@@ -54,6 +62,13 @@ END_DESC
desc <<-END_DESC desc <<-END_DESC
Read emails from an IMAP server. Read emails from an IMAP server.
General options:
unknown_user=ACTION how to handle emails from an unknown user
ACTION can be one of the following values:
ignore: email is ignored (default)
accept: accept as anonymous user
create: create a user account
Available IMAP options: Available IMAP options:
host=HOST IMAP server host (default: 127.0.0.1) host=HOST IMAP server host (default: 127.0.0.1)
port=PORT IMAP server port (default: 143) port=PORT IMAP server port (default: 143)
...@@ -61,7 +76,7 @@ Available IMAP options: ...@@ -61,7 +76,7 @@ Available IMAP options:
username=USERNAME IMAP account username=USERNAME IMAP account
password=PASSWORD IMAP password password=PASSWORD IMAP password
folder=FOLDER IMAP folder to read (default: INBOX) folder=FOLDER IMAP folder to read (default: INBOX)
Issue attributes control options: Issue attributes control options:
project=PROJECT identifier of the target project project=PROJECT identifier of the target project
status=STATUS name of the target status status=STATUS name of the target status
...@@ -107,6 +122,7 @@ END_DESC ...@@ -107,6 +122,7 @@ END_DESC
options = { :issue => {} } options = { :issue => {} }
%w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user']
Redmine::IMAP.check(imap_options, options) Redmine::IMAP.check(imap_options, options)
end end
......
Return-Path: <john.doe@somenet.foo>
Received: from osiris ([127.0.0.1])
by OSIRIS
with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
From: "John Doe" <john.doe@somenet.foo>
To: <redmine@somenet.foo>
Subject: Ticket by unknown user
Date: Sun, 22 Jun 2008 12:28:07 +0200
MIME-Version: 1.0
Content-Type: text/plain;
format=flowed;
charset="iso-8859-1";
reply-type=original
Content-Transfer-Encoding: 7bit
This is a ticket submitted by an unknown user.
# redMine - project management software # Redmine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang # Copyright (C) 2006-2009 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
...@@ -130,6 +130,41 @@ class MailHandlerTest < Test::Unit::TestCase ...@@ -130,6 +130,41 @@ class MailHandlerTest < Test::Unit::TestCase
assert_equal 1, issue.watchers.size assert_equal 1, issue.watchers.size
end end
def test_add_issue_by_unknown_user
assert_no_difference 'User.count' do
assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
end
end
def test_add_issue_by_anonymous_user
Role.anonymous.add_permission!(:add_issues)
assert_no_difference 'User.count' do
issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
assert issue.is_a?(Issue)
assert issue.author.anonymous?
end
end
def test_add_issue_by_created_user
Setting.default_language = 'en'
assert_difference 'User.count' do
issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
assert issue.is_a?(Issue)
assert issue.author.active?
assert_equal 'john.doe@somenet.foo', issue.author.mail
assert_equal 'John', issue.author.firstname
assert_equal 'Doe', issue.author.lastname
# account information
email = ActionMailer::Base.deliveries.first
assert_not_nil email
assert email.subject.include?('account activation')
login = email.body.match(/\* Login: (.*)$/)[1]
password = email.body.match(/\* Password: (.*)$/)[1]
assert_equal issue.author, User.try_to_login(login, password)
end
end
def test_add_issue_without_from_header def test_add_issue_without_from_header
Role.anonymous.add_permission!(:add_issues) Role.anonymous.add_permission!(:add_issues)
assert_equal false, submit_email('ticket_without_from_header.eml') assert_equal false, submit_email('ticket_without_from_header.eml')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment