Chris@0: # redMine - project management software Chris@0: # Copyright (C) 2006-2007 Jean-Philippe Lang Chris@0: # Chris@0: # This program is free software; you can redistribute it and/or Chris@0: # modify it under the terms of the GNU General Public License Chris@0: # as published by the Free Software Foundation; either version 2 Chris@0: # of the License, or (at your option) any later version. Chris@0: # Chris@0: # This program is distributed in the hope that it will be useful, Chris@0: # but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@0: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@0: # GNU General Public License for more details. Chris@0: # Chris@0: # You should have received a copy of the GNU General Public License Chris@0: # along with this program; if not, write to the Free Software Chris@0: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Chris@0: Chris@0: class MailHandler < ActionMailer::Base Chris@0: include ActionView::Helpers::SanitizeHelper Chris@0: Chris@0: class UnauthorizedAction < StandardError; end Chris@0: class MissingInformation < StandardError; end Chris@0: Chris@0: attr_reader :email, :user Chris@0: Chris@0: def self.receive(email, options={}) Chris@0: @@handler_options = options.dup Chris@0: Chris@0: @@handler_options[:issue] ||= {} Chris@0: Chris@0: @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String) Chris@0: @@handler_options[:allow_override] ||= [] Chris@0: # Project needs to be overridable if not specified Chris@0: @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) Chris@0: # Status overridable by default Chris@0: @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) Chris@0: Chris@0: @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false) Chris@0: super email Chris@0: end Chris@0: Chris@0: # Processes incoming emails Chris@0: # Returns the created object (eg. an issue, a message) or false Chris@0: def receive(email) Chris@0: @email = email Chris@0: sender_email = email.from.to_a.first.to_s.strip Chris@0: # Ignore emails received from the application emission address to avoid hell cycles Chris@0: if sender_email.downcase == Setting.mail_from.to_s.strip.downcase Chris@0: logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info Chris@0: return false Chris@0: end Chris@0: @user = User.find_by_mail(sender_email) if sender_email.present? Chris@0: if @user && !@user.active? Chris@0: logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info Chris@0: return false Chris@0: end Chris@0: if @user.nil? Chris@0: # Email was submitted by an unknown user Chris@0: case @@handler_options[:unknown_user] Chris@0: when 'accept' Chris@0: @user = User.anonymous Chris@0: when 'create' Chris@0: @user = MailHandler.create_user_from_email(email) Chris@0: if @user Chris@0: logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info Chris@0: Mailer.deliver_account_information(@user, @user.password) Chris@0: else Chris@0: logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error Chris@0: return false Chris@0: end Chris@0: else Chris@0: # Default behaviour, emails from unknown users are ignored Chris@0: logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info Chris@0: return false Chris@0: end Chris@0: end Chris@0: User.current = @user Chris@0: dispatch Chris@0: end Chris@0: Chris@0: private Chris@0: Chris@0: MESSAGE_ID_RE = %r{^ e Chris@0: # TODO: send a email to the user Chris@0: logger.error e.message if logger Chris@0: false Chris@0: rescue MissingInformation => e Chris@0: logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger Chris@0: false Chris@0: rescue UnauthorizedAction => e Chris@0: logger.error "MailHandler: unauthorized attempt from #{user}" if logger Chris@0: false Chris@0: end Chris@0: Chris@0: # Creates a new issue Chris@0: def receive_issue Chris@0: project = target_project Chris@0: tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first) Chris@0: category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category))) Chris@0: priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority))) Chris@0: status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) Chris@0: assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true))) Chris@0: due_date = get_keyword(:due_date, :override => true) Chris@0: start_date = get_keyword(:start_date, :override => true) Chris@0: Chris@0: # check permission Chris@0: unless @@handler_options[:no_permission_check] Chris@0: raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) Chris@0: end Chris@0: Chris@0: issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :due_date => due_date, :start_date => start_date, :assigned_to => assigned_to) Chris@0: # check workflow Chris@0: if status && issue.new_statuses_allowed_to(user).include?(status) Chris@0: issue.status = status Chris@0: end Chris@0: issue.subject = email.subject.chomp[0,255] Chris@0: if issue.subject.blank? Chris@0: issue.subject = '(no subject)' Chris@0: end Chris@0: # custom fields Chris@0: issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c| Chris@0: if value = get_keyword(c.name, :override => true) Chris@0: h[c.id] = value Chris@0: end Chris@0: h Chris@0: end Chris@0: issue.description = cleaned_up_text_body Chris@0: # add To and Cc as watchers before saving so the watchers can reply to Redmine Chris@0: add_watchers(issue) Chris@0: issue.save! Chris@0: add_attachments(issue) Chris@0: logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info Chris@0: issue Chris@0: end Chris@0: Chris@0: def target_project Chris@0: # TODO: other ways to specify project: Chris@0: # * parse the email To field Chris@0: # * specific project (eg. Setting.mail_handler_target_project) Chris@0: target = Project.find_by_identifier(get_keyword(:project)) Chris@0: raise MissingInformation.new('Unable to determine target project') if target.nil? Chris@0: target Chris@0: end Chris@0: Chris@0: # Adds a note to an existing issue Chris@0: def receive_issue_reply(issue_id) Chris@0: status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) Chris@0: due_date = get_keyword(:due_date, :override => true) Chris@0: start_date = get_keyword(:start_date, :override => true) Chris@0: assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true))) Chris@0: Chris@0: issue = Issue.find_by_id(issue_id) Chris@0: return unless issue Chris@0: # check permission Chris@0: unless @@handler_options[:no_permission_check] Chris@0: raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) Chris@0: raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project) Chris@0: end Chris@0: Chris@0: # add the note Chris@0: journal = issue.init_journal(user, cleaned_up_text_body) Chris@0: add_attachments(issue) Chris@0: # check workflow Chris@0: if status && issue.new_statuses_allowed_to(user).include?(status) Chris@0: issue.status = status Chris@0: end Chris@0: issue.start_date = start_date if start_date Chris@0: issue.due_date = due_date if due_date Chris@0: issue.assigned_to = assigned_to if assigned_to Chris@0: Chris@0: issue.save! Chris@0: logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info Chris@0: journal Chris@0: end Chris@0: Chris@0: # Reply will be added to the issue Chris@0: def receive_journal_reply(journal_id) Chris@0: journal = Journal.find_by_id(journal_id) Chris@0: if journal && journal.journalized_type == 'Issue' Chris@0: receive_issue_reply(journal.journalized_id) Chris@0: end Chris@0: end Chris@0: Chris@0: # Receives a reply to a forum message Chris@0: def receive_message_reply(message_id) Chris@0: message = Message.find_by_id(message_id) Chris@0: if message Chris@0: message = message.root Chris@0: Chris@0: unless @@handler_options[:no_permission_check] Chris@0: raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project) Chris@0: end Chris@0: Chris@0: if !message.locked? Chris@0: reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip, Chris@0: :content => cleaned_up_text_body) Chris@0: reply.author = user Chris@0: reply.board = message.board Chris@0: message.children << reply Chris@0: add_attachments(reply) Chris@0: reply Chris@0: else Chris@0: logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info Chris@0: end Chris@0: end Chris@0: end Chris@0: Chris@0: def add_attachments(obj) Chris@0: if email.has_attachments? Chris@0: email.attachments.each do |attachment| Chris@0: Attachment.create(:container => obj, Chris@0: :file => attachment, Chris@0: :author => user, Chris@0: :content_type => attachment.content_type) Chris@0: end Chris@0: end Chris@0: end Chris@0: Chris@0: # Adds To and Cc as watchers of the given object if the sender has the Chris@0: # appropriate permission Chris@0: def add_watchers(obj) Chris@0: if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) Chris@0: addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} Chris@0: unless addresses.empty? Chris@0: watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses]) Chris@0: watchers.each {|w| obj.add_watcher(w)} Chris@0: end Chris@0: end Chris@0: end Chris@0: Chris@0: def get_keyword(attr, options={}) Chris@0: @keywords ||= {} Chris@0: if @keywords.has_key?(attr) Chris@0: @keywords[attr] Chris@0: else Chris@0: @keywords[attr] = begin Chris@0: if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr.to_s.humanize}[ \t]*:[ \t]*(.+)\s*$/i, '') Chris@0: $1.strip Chris@0: elsif !@@handler_options[:issue][attr].blank? Chris@0: @@handler_options[:issue][attr] Chris@0: end Chris@0: end Chris@0: end Chris@0: end Chris@0: Chris@0: # Returns the text/plain part of the email Chris@0: # If not found (eg. HTML-only email), returns the body with tags removed Chris@0: def plain_text_body Chris@0: return @plain_text_body unless @plain_text_body.nil? Chris@0: parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten Chris@0: if parts.empty? Chris@0: parts << @email Chris@0: end Chris@0: plain_text_part = parts.detect {|p| p.content_type == 'text/plain'} Chris@0: if plain_text_part.nil? Chris@0: # no text/plain part found, assuming html-only email Chris@0: # strip html tags and remove doctype directive Chris@0: @plain_text_body = strip_tags(@email.body.to_s) Chris@0: @plain_text_body.gsub! %r{^