##// END OF EJS Templates
Do not try to copy relations for issues that could not be copied....
Do not try to copy relations for issues that could not be copied. git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@4484 e93f8b46-1217-0410-a6f0-8f06a7374b81

File last commit:

r4290:d7cdd58db6b8
r4370:cd71c1cc0ad7
Show More
mail_handler.rb
352 lines | 13.1 KiB | text/x-ruby | RubyLexer
Jean-Philippe Lang
Added a simple mail handler....
r520 # redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MailHandler < ActionMailer::Base
Jean-Philippe Lang
Mail handler: strip tags when receiving a html-only email (#2312)....
r2134 include ActionView::Helpers::SanitizeHelper
Jean-Philippe Lang
Makes MailHandler accept localized keywords for default or user language (#6112)....
r4281 include Redmine::I18n
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554
class UnauthorizedAction < StandardError; end
class MissingInformation < StandardError; end
attr_reader :email, :user
def self.receive(email, options={})
Jean-Philippe Lang
Mail handler: more control over issue attributes (#1110)....
r1629 @@handler_options = options.dup
@@handler_options[:issue] ||= {}
@@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
@@handler_options[:allow_override] ||= []
# Project needs to be overridable if not specified
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
Jean-Philippe Lang
Mail handler: check workflow for status set/change....
r2072 # Status overridable by default
Eric Davis
Added the "Status:" keyword to the MailHandler for setting and changing an Issue status via email....
r1750 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
Jean-Philippe Lang
Adds a 'no_permission_check' option to the MailHandler....
r3081
@@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 super email
end
Jean-Philippe Lang
Added a simple mail handler....
r520
# Processes incoming emails
Jean-Philippe Lang
Ability to accept incoming emails from unknown users (#2230, #3003)....
r2689 # Returns the created object (eg. an issue, a message) or false
Jean-Philippe Lang
Added a simple mail handler....
r520 def receive(email)
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 @email = email
Jean-Philippe Lang
Ignore emails received from the application emission address to avoid hell cycles (#4139)....
r2908 sender_email = email.from.to_a.first.to_s.strip
# Ignore emails received from the application emission address to avoid hell cycles
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
return false
end
Eric Davis
Accept email from anonymous users with an empty from address. #5604...
r3669 @user = User.find_by_mail(sender_email) if sender_email.present?
Jean-Philippe Lang
Ability to accept incoming emails from unknown users (#2230, #3003)....
r2689 if @user && !@user.active?
logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 return false
end
Jean-Philippe Lang
Ability to accept incoming emails from unknown users (#2230, #3003)....
r2689 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
Jean-Philippe Lang
Ignore emails received from the application emission address to avoid hell cycles (#4139)....
r2908 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
Jean-Philippe Lang
Ability to accept incoming emails from unknown users (#2230, #3003)....
r2689 return false
end
else
# Default behaviour, emails from unknown users are ignored
Jean-Philippe Lang
Ignore emails received from the application emission address to avoid hell cycles (#4139)....
r2908 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
Jean-Philippe Lang
Ability to accept incoming emails from unknown users (#2230, #3003)....
r2689 return false
end
end
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 User.current = @user
dispatch
end
private
Jean-Philippe Lang
Use In-Reply-To and References headers to handle replies by email....
r2286 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
Jean-Philippe Lang
Allow [#id] as subject to reply by email (#3653)....
r2917 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554
def dispatch
Jean-Philippe Lang
Use In-Reply-To and References headers to handle replies by email....
r2286 headers = [email.in_reply_to, email.references].flatten.compact
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
klass, object_id = $1, $2.to_i
method_name = "receive_#{klass}_reply"
Jean-Philippe Lang
Fixes MailHandler for ruby1.9....
r2801 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
Jean-Philippe Lang
Use In-Reply-To and References headers to handle replies by email....
r2286 send method_name, object_id
else
# ignoring it
end
elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
receive_issue_reply(m[1].to_i)
Jean-Philippe Lang
Accept replies to forum messages by subject recognition (#1616)....
r2292 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
receive_message_reply(m[1].to_i)
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 else
receive_issue
end
rescue ActiveRecord::RecordInvalid => e
# TODO: send a email to the user
logger.error e.message if logger
false
rescue MissingInformation => e
logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
false
rescue UnauthorizedAction => e
logger.error "MailHandler: unauthorized attempt from #{user}" if logger
false
end
# Creates a new issue
def receive_issue
project = target_project
# check permission
Jean-Philippe Lang
Adds a 'no_permission_check' option to the MailHandler....
r3081 unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
end
Eric Davis
Add 'Start date' and 'End date' keywords for incoming email. #5595...
r3649
Jean-Philippe Lang
Makes MailHandler accept all issue attributes and custom fields that can be set/updated (#4071, #4807, #5622, #6110)....
r4280 issue = Issue.new(:author => user, :project => project)
issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
issue.subject = email.subject.to_s.chomp[0,255]
Jean-Philippe Lang
Email handler: set a default issue subject if the email subject is blank (#3850)....
r2759 if issue.subject.blank?
issue.subject = '(no subject)'
end
Jean-Philippe Lang
Adds a setting to remove incoming emails body after a delimiter (#4409)....
r3112 issue.description = cleaned_up_text_body
Jean-Philippe Lang
Makes MailHandler accept all issue attributes and custom fields that can be set/updated (#4071, #4807, #5622, #6110)....
r4280
Eric Davis
Added observers to watch model objects for mail delivery instead of calling Mailer....
r2548 # add To and Cc as watchers before saving so the watchers can reply to Redmine
add_watchers(issue)
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 issue.save!
add_attachments(issue)
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
issue
end
# Adds a note to an existing issue
Jean-Philippe Lang
Use In-Reply-To and References headers to handle replies by email....
r2286 def receive_issue_reply(issue_id)
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 issue = Issue.find_by_id(issue_id)
Jean-Philippe Lang
Added a simple mail handler....
r520 return unless issue
# check permission
Jean-Philippe Lang
Adds a 'no_permission_check' option to the MailHandler....
r3081 unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
end
Jean-Philippe Lang
Makes MailHandler accept all issue attributes and custom fields that can be set/updated (#4071, #4807, #5622, #6110)....
r4280
Jean-Philippe Lang
Adds a setting to remove incoming emails body after a delimiter (#4409)....
r3112 journal = issue.init_journal(user, cleaned_up_text_body)
Jean-Philippe Lang
Makes MailHandler accept all issue attributes and custom fields that can be set/updated (#4071, #4807, #5622, #6110)....
r4280 issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
Jean-Philippe Lang
Fixes reply attachments handling....
r1556 add_attachments(issue)
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
journal
end
Jean-Philippe Lang
Use In-Reply-To and References headers to handle replies by email....
r2286 # Reply will be added to the issue
def receive_journal_reply(journal_id)
journal = Journal.find_by_id(journal_id)
if journal && journal.journalized_type == 'Issue'
receive_issue_reply(journal.journalized_id)
end
end
Jean-Philippe Lang
Allow email to reply to a forum message (#1616)....
r2287 # Receives a reply to a forum message
def receive_message_reply(message_id)
message = Message.find_by_id(message_id)
if message
message = message.root
Jean-Philippe Lang
Adds a 'no_permission_check' option to the MailHandler....
r3081
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
end
if !message.locked?
Jean-Philippe Lang
Accept replies to forum messages by subject recognition (#1616)....
r2292 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
Jean-Philippe Lang
Adds a setting to remove incoming emails body after a delimiter (#4409)....
r3112 :content => cleaned_up_text_body)
Jean-Philippe Lang
Allow email to reply to a forum message (#1616)....
r2287 reply.author = user
reply.board = message.board
message.children << reply
add_attachments(reply)
reply
else
Jean-Philippe Lang
Adds a 'no_permission_check' option to the MailHandler....
r3081 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
Jean-Philippe Lang
Allow email to reply to a forum message (#1616)....
r2287 end
end
end
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 def add_attachments(obj)
if email.has_attachments?
email.attachments.each do |attachment|
Attachment.create(:container => obj,
:file => attachment,
:author => user,
:content_type => attachment.content_type)
end
end
Jean-Philippe Lang
Added a simple mail handler....
r520 end
Jean-Philippe Lang
Mail handler: more control over issue attributes (#1110)....
r1629
Jean-Philippe Lang
Adds To and Cc as watchers when submitting an issue by email (#2245)....
r2075 # Adds To and Cc as watchers of the given object if the sender has the
# appropriate permission
def add_watchers(obj)
if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
unless addresses.empty?
Jean-Philippe Lang
Replaces User.find_active with a named scope....
r2077 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
Jean-Philippe Lang
Adds To and Cc as watchers when submitting an issue by email (#2245)....
r2075 watchers.each {|w| obj.add_watcher(w)}
end
end
end
Jean-Philippe Lang
Import custom fields values from emails (#2413)....
r2209 def get_keyword(attr, options={})
Jean-Philippe Lang
Strip keywords from received email body (#2436)....
r2365 @keywords ||= {}
if @keywords.has_key?(attr)
@keywords[attr]
else
@keywords[attr] = begin
Jean-Philippe Lang
Makes MailHandler ignore invalid keyword values to avoid validation failures....
r4282 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
Jean-Philippe Lang
Makes MailHandler accept localized keywords for default or user language (#6112)....
r4281 v
Jean-Philippe Lang
Strip keywords from received email body (#2436)....
r2365 elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr]
end
end
Jean-Philippe Lang
Mail handler: more control over issue attributes (#1110)....
r1629 end
end
Jean-Philippe Lang
Makes MailHandler accept localized keywords for default or user language (#6112)....
r4281
# Destructively extracts the value for +attr+ in +text+
# Returns nil if no matching keyword found
Jean-Philippe Lang
Makes MailHandler ignore invalid keyword values to avoid validation failures....
r4282 def extract_keyword!(text, attr, format=nil)
Jean-Philippe Lang
Makes MailHandler accept localized keywords for default or user language (#6112)....
r4281 keys = [attr.to_s.humanize]
if attr.is_a?(Symbol)
keys << l("field_#{attr}", :default => '', :locale => user.language) if user
keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
end
keys.reject! {|k| k.blank?}
keys.collect! {|k| Regexp.escape(k)}
Jean-Philippe Lang
Makes MailHandler ignore invalid keyword values to avoid validation failures....
r4282 format ||= '.+'
text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
Jean-Philippe Lang
Makes MailHandler accept localized keywords for default or user language (#6112)....
r4281 $2 && $2.strip
end
Jean-Philippe Lang
Makes MailHandler accept all issue attributes and custom fields that can be set/updated (#4071, #4807, #5622, #6110)....
r4280
def target_project
# TODO: other ways to specify project:
# * parse the email To field
# * specific project (eg. Setting.mail_handler_target_project)
target = Project.find_by_identifier(get_keyword(:project))
raise MissingInformation.new('Unable to determine target project') if target.nil?
target
end
# Returns a Hash of issue attributes extracted from keywords in the email body
def issue_attributes_from_keywords(issue)
Jean-Philippe Lang
MailHandler: ignore assignee if invalid....
r4290 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
Jean-Philippe Lang
Makes MailHandler accept all issue attributes and custom fields that can be set/updated (#4071, #4807, #5622, #6110)....
r4280 {
'tracker_id' => ((k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id)) || issue.project.trackers.find(:first).try(:id),
'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
Jean-Philippe Lang
MailHandler: ignore assignee if invalid....
r4290 'assigned_to_id' => assigned_to.try(:id),
Jean-Philippe Lang
Makes MailHandler accept all issue attributes and custom fields that can be set/updated (#4071, #4807, #5622, #6110)....
r4280 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
Jean-Philippe Lang
Makes MailHandler ignore invalid keyword values to avoid validation failures....
r4282 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
Jean-Philippe Lang
Makes MailHandler accept all issue attributes and custom fields that can be set/updated (#4071, #4807, #5622, #6110)....
r4280 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
Jean-Philippe Lang
Makes MailHandler ignore invalid keyword values to avoid validation failures....
r4282 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
Jean-Philippe Lang
Makes MailHandler accept all issue attributes and custom fields that can be set/updated (#4071, #4807, #5622, #6110)....
r4280 }.delete_if {|k, v| v.blank? }
end
# Returns a Hash of issue custom field values extracted from keywords in the email body
def custom_field_values_from_keywords(customized)
customized.custom_field_values.inject({}) do |h, v|
if value = get_keyword(v.custom_field.name, :override => true)
h[v.custom_field.id.to_s] = value
end
h
end
end
Jean-Philippe Lang
Mail handler: strip tags when receiving a html-only email (#2312)....
r2134
# Returns the text/plain part of the email
# If not found (eg. HTML-only email), returns the body with tags removed
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 def plain_text_body
return @plain_text_body unless @plain_text_body.nil?
Jean-Philippe Lang
Mail handler: strip tags when receiving a html-only email (#2312)....
r2134 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
if parts.empty?
parts << @email
end
plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
if plain_text_part.nil?
# no text/plain part found, assuming html-only email
# strip html tags and remove doctype directive
@plain_text_body = strip_tags(@email.body.to_s)
@plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
else
@plain_text_body = plain_text_part.body.to_s
end
@plain_text_body.strip!
Jean-Philippe Lang
Fixed that MailHandler#plain_text_body was returning nil if there was nothing to strip (#2814)....
r2456 @plain_text_body
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 end
Jean-Philippe Lang
Merged Rails 2.2 branch. Redmine now requires Rails 2.2.2....
r2430
Jean-Philippe Lang
Adds a setting to remove incoming emails body after a delimiter (#4409)....
r3112 def cleaned_up_text_body
cleanup_body(plain_text_body)
end
Jean-Philippe Lang
Merged Rails 2.2 branch. Redmine now requires Rails 2.2.2....
r2430
def self.full_sanitizer
@full_sanitizer ||= HTML::FullSanitizer.new
end
Jean-Philippe Lang
Ability to accept incoming emails from unknown users (#2230, #3003)....
r2689
# 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
Jean-Philippe Lang
Adds a setting to remove incoming emails body after a delimiter (#4409)....
r3112
private
# Removes the email body of text after the truncation configurations.
def cleanup_body(body)
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
unless delimiters.empty?
Eric Davis
Remove email quotes (>) when searching for incoming email delimiters. #2852 #6628...
r4247 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
Jean-Philippe Lang
Adds a setting to remove incoming emails body after a delimiter (#4409)....
r3112 body = body.gsub(regex, '')
end
body.strip
end
Eric Davis
Add an "Assigned To" keyword to receiving email. #5594...
r3650
def find_user_from_keyword(keyword)
user ||= User.find_by_mail(keyword)
user ||= User.find_by_login(keyword)
if user.nil? && keyword.match(/ /)
firstname, lastname = *(keyword.split) # "First Last Throwaway"
user ||= User.find_by_firstname_and_lastname(firstname, lastname)
end
user
end
Jean-Philippe Lang
Adds basic support for issue creation via email (#1110)....
r1554 end