annotate app/models/mail_handler.rb @ 8:0c83d98252d9 yuya

* Add custom repo prefix and proper auth realm, remove auth cache (seems like an unwise feature), pass DB handle around, various other bits of tidying
author Chris Cannam
date Thu, 12 Aug 2010 15:31:37 +0100
parents 513646585e45
children 94944d00e43c
rev   line source
Chris@0 1 # redMine - project management software
Chris@0 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@0 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@0 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 class MailHandler < ActionMailer::Base
Chris@0 19 include ActionView::Helpers::SanitizeHelper
Chris@0 20
Chris@0 21 class UnauthorizedAction < StandardError; end
Chris@0 22 class MissingInformation < StandardError; end
Chris@0 23
Chris@0 24 attr_reader :email, :user
Chris@0 25
Chris@0 26 def self.receive(email, options={})
Chris@0 27 @@handler_options = options.dup
Chris@0 28
Chris@0 29 @@handler_options[:issue] ||= {}
Chris@0 30
Chris@0 31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
Chris@0 32 @@handler_options[:allow_override] ||= []
Chris@0 33 # Project needs to be overridable if not specified
Chris@0 34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
Chris@0 35 # Status overridable by default
Chris@0 36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
Chris@0 37
Chris@0 38 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
Chris@0 39 super email
Chris@0 40 end
Chris@0 41
Chris@0 42 # Processes incoming emails
Chris@0 43 # Returns the created object (eg. an issue, a message) or false
Chris@0 44 def receive(email)
Chris@0 45 @email = email
Chris@0 46 sender_email = email.from.to_a.first.to_s.strip
Chris@0 47 # Ignore emails received from the application emission address to avoid hell cycles
Chris@0 48 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
Chris@0 49 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
Chris@0 50 return false
Chris@0 51 end
Chris@0 52 @user = User.find_by_mail(sender_email) if sender_email.present?
Chris@0 53 if @user && !@user.active?
Chris@0 54 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
Chris@0 55 return false
Chris@0 56 end
Chris@0 57 if @user.nil?
Chris@0 58 # Email was submitted by an unknown user
Chris@0 59 case @@handler_options[:unknown_user]
Chris@0 60 when 'accept'
Chris@0 61 @user = User.anonymous
Chris@0 62 when 'create'
Chris@0 63 @user = MailHandler.create_user_from_email(email)
Chris@0 64 if @user
Chris@0 65 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
Chris@0 66 Mailer.deliver_account_information(@user, @user.password)
Chris@0 67 else
Chris@0 68 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
Chris@0 69 return false
Chris@0 70 end
Chris@0 71 else
Chris@0 72 # Default behaviour, emails from unknown users are ignored
Chris@0 73 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
Chris@0 74 return false
Chris@0 75 end
Chris@0 76 end
Chris@0 77 User.current = @user
Chris@0 78 dispatch
Chris@0 79 end
Chris@0 80
Chris@0 81 private
Chris@0 82
Chris@0 83 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
Chris@0 84 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
Chris@0 85 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
Chris@0 86
Chris@0 87 def dispatch
Chris@0 88 headers = [email.in_reply_to, email.references].flatten.compact
Chris@0 89 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
Chris@0 90 klass, object_id = $1, $2.to_i
Chris@0 91 method_name = "receive_#{klass}_reply"
Chris@0 92 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
Chris@0 93 send method_name, object_id
Chris@0 94 else
Chris@0 95 # ignoring it
Chris@0 96 end
Chris@0 97 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
Chris@0 98 receive_issue_reply(m[1].to_i)
Chris@0 99 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
Chris@0 100 receive_message_reply(m[1].to_i)
Chris@0 101 else
Chris@0 102 receive_issue
Chris@0 103 end
Chris@0 104 rescue ActiveRecord::RecordInvalid => e
Chris@0 105 # TODO: send a email to the user
Chris@0 106 logger.error e.message if logger
Chris@0 107 false
Chris@0 108 rescue MissingInformation => e
Chris@0 109 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
Chris@0 110 false
Chris@0 111 rescue UnauthorizedAction => e
Chris@0 112 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
Chris@0 113 false
Chris@0 114 end
Chris@0 115
Chris@0 116 # Creates a new issue
Chris@0 117 def receive_issue
Chris@0 118 project = target_project
Chris@0 119 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
Chris@0 120 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
Chris@0 121 priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
Chris@0 122 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
Chris@0 123 assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true)))
Chris@0 124 due_date = get_keyword(:due_date, :override => true)
Chris@0 125 start_date = get_keyword(:start_date, :override => true)
Chris@0 126
Chris@0 127 # check permission
Chris@0 128 unless @@handler_options[:no_permission_check]
Chris@0 129 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
Chris@0 130 end
Chris@0 131
Chris@0 132 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 133 # check workflow
Chris@0 134 if status && issue.new_statuses_allowed_to(user).include?(status)
Chris@0 135 issue.status = status
Chris@0 136 end
Chris@0 137 issue.subject = email.subject.chomp[0,255]
Chris@0 138 if issue.subject.blank?
Chris@0 139 issue.subject = '(no subject)'
Chris@0 140 end
Chris@0 141 # custom fields
Chris@0 142 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
Chris@0 143 if value = get_keyword(c.name, :override => true)
Chris@0 144 h[c.id] = value
Chris@0 145 end
Chris@0 146 h
Chris@0 147 end
Chris@0 148 issue.description = cleaned_up_text_body
Chris@0 149 # add To and Cc as watchers before saving so the watchers can reply to Redmine
Chris@0 150 add_watchers(issue)
Chris@0 151 issue.save!
Chris@0 152 add_attachments(issue)
Chris@0 153 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
Chris@0 154 issue
Chris@0 155 end
Chris@0 156
Chris@0 157 def target_project
Chris@0 158 # TODO: other ways to specify project:
Chris@0 159 # * parse the email To field
Chris@0 160 # * specific project (eg. Setting.mail_handler_target_project)
Chris@0 161 target = Project.find_by_identifier(get_keyword(:project))
Chris@0 162 raise MissingInformation.new('Unable to determine target project') if target.nil?
Chris@0 163 target
Chris@0 164 end
Chris@0 165
Chris@0 166 # Adds a note to an existing issue
Chris@0 167 def receive_issue_reply(issue_id)
Chris@0 168 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
Chris@0 169 due_date = get_keyword(:due_date, :override => true)
Chris@0 170 start_date = get_keyword(:start_date, :override => true)
Chris@0 171 assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true)))
Chris@0 172
Chris@0 173 issue = Issue.find_by_id(issue_id)
Chris@0 174 return unless issue
Chris@0 175 # check permission
Chris@0 176 unless @@handler_options[:no_permission_check]
Chris@0 177 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
Chris@0 178 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
Chris@0 179 end
Chris@0 180
Chris@0 181 # add the note
Chris@0 182 journal = issue.init_journal(user, cleaned_up_text_body)
Chris@0 183 add_attachments(issue)
Chris@0 184 # check workflow
Chris@0 185 if status && issue.new_statuses_allowed_to(user).include?(status)
Chris@0 186 issue.status = status
Chris@0 187 end
Chris@0 188 issue.start_date = start_date if start_date
Chris@0 189 issue.due_date = due_date if due_date
Chris@0 190 issue.assigned_to = assigned_to if assigned_to
Chris@0 191
Chris@0 192 issue.save!
Chris@0 193 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
Chris@0 194 journal
Chris@0 195 end
Chris@0 196
Chris@0 197 # Reply will be added to the issue
Chris@0 198 def receive_journal_reply(journal_id)
Chris@0 199 journal = Journal.find_by_id(journal_id)
Chris@0 200 if journal && journal.journalized_type == 'Issue'
Chris@0 201 receive_issue_reply(journal.journalized_id)
Chris@0 202 end
Chris@0 203 end
Chris@0 204
Chris@0 205 # Receives a reply to a forum message
Chris@0 206 def receive_message_reply(message_id)
Chris@0 207 message = Message.find_by_id(message_id)
Chris@0 208 if message
Chris@0 209 message = message.root
Chris@0 210
Chris@0 211 unless @@handler_options[:no_permission_check]
Chris@0 212 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
Chris@0 213 end
Chris@0 214
Chris@0 215 if !message.locked?
Chris@0 216 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
Chris@0 217 :content => cleaned_up_text_body)
Chris@0 218 reply.author = user
Chris@0 219 reply.board = message.board
Chris@0 220 message.children << reply
Chris@0 221 add_attachments(reply)
Chris@0 222 reply
Chris@0 223 else
Chris@0 224 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
Chris@0 225 end
Chris@0 226 end
Chris@0 227 end
Chris@0 228
Chris@0 229 def add_attachments(obj)
Chris@0 230 if email.has_attachments?
Chris@0 231 email.attachments.each do |attachment|
Chris@0 232 Attachment.create(:container => obj,
Chris@0 233 :file => attachment,
Chris@0 234 :author => user,
Chris@0 235 :content_type => attachment.content_type)
Chris@0 236 end
Chris@0 237 end
Chris@0 238 end
Chris@0 239
Chris@0 240 # Adds To and Cc as watchers of the given object if the sender has the
Chris@0 241 # appropriate permission
Chris@0 242 def add_watchers(obj)
Chris@0 243 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
Chris@0 244 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
Chris@0 245 unless addresses.empty?
Chris@0 246 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
Chris@0 247 watchers.each {|w| obj.add_watcher(w)}
Chris@0 248 end
Chris@0 249 end
Chris@0 250 end
Chris@0 251
Chris@0 252 def get_keyword(attr, options={})
Chris@0 253 @keywords ||= {}
Chris@0 254 if @keywords.has_key?(attr)
Chris@0 255 @keywords[attr]
Chris@0 256 else
Chris@0 257 @keywords[attr] = begin
Chris@0 258 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 259 $1.strip
Chris@0 260 elsif !@@handler_options[:issue][attr].blank?
Chris@0 261 @@handler_options[:issue][attr]
Chris@0 262 end
Chris@0 263 end
Chris@0 264 end
Chris@0 265 end
Chris@0 266
Chris@0 267 # Returns the text/plain part of the email
Chris@0 268 # If not found (eg. HTML-only email), returns the body with tags removed
Chris@0 269 def plain_text_body
Chris@0 270 return @plain_text_body unless @plain_text_body.nil?
Chris@0 271 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
Chris@0 272 if parts.empty?
Chris@0 273 parts << @email
Chris@0 274 end
Chris@0 275 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
Chris@0 276 if plain_text_part.nil?
Chris@0 277 # no text/plain part found, assuming html-only email
Chris@0 278 # strip html tags and remove doctype directive
Chris@0 279 @plain_text_body = strip_tags(@email.body.to_s)
Chris@0 280 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
Chris@0 281 else
Chris@0 282 @plain_text_body = plain_text_part.body.to_s
Chris@0 283 end
Chris@0 284 @plain_text_body.strip!
Chris@0 285 @plain_text_body
Chris@0 286 end
Chris@0 287
Chris@0 288 def cleaned_up_text_body
Chris@0 289 cleanup_body(plain_text_body)
Chris@0 290 end
Chris@0 291
Chris@0 292 def self.full_sanitizer
Chris@0 293 @full_sanitizer ||= HTML::FullSanitizer.new
Chris@0 294 end
Chris@0 295
Chris@0 296 # Creates a user account for the +email+ sender
Chris@0 297 def self.create_user_from_email(email)
Chris@0 298 addr = email.from_addrs.to_a.first
Chris@0 299 if addr && !addr.spec.blank?
Chris@0 300 user = User.new
Chris@0 301 user.mail = addr.spec
Chris@0 302
Chris@0 303 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
Chris@0 304 user.firstname = names.shift
Chris@0 305 user.lastname = names.join(' ')
Chris@0 306 user.lastname = '-' if user.lastname.blank?
Chris@0 307
Chris@0 308 user.login = user.mail
Chris@0 309 user.password = ActiveSupport::SecureRandom.hex(5)
Chris@0 310 user.language = Setting.default_language
Chris@0 311 user.save ? user : nil
Chris@0 312 end
Chris@0 313 end
Chris@0 314
Chris@0 315 private
Chris@0 316
Chris@0 317 # Removes the email body of text after the truncation configurations.
Chris@0 318 def cleanup_body(body)
Chris@0 319 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
Chris@0 320 unless delimiters.empty?
Chris@0 321 regex = Regexp.new("^(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
Chris@0 322 body = body.gsub(regex, '')
Chris@0 323 end
Chris@0 324 body.strip
Chris@0 325 end
Chris@0 326
Chris@0 327 def find_user_from_keyword(keyword)
Chris@0 328 user ||= User.find_by_mail(keyword)
Chris@0 329 user ||= User.find_by_login(keyword)
Chris@0 330 if user.nil? && keyword.match(/ /)
Chris@0 331 firstname, lastname = *(keyword.split) # "First Last Throwaway"
Chris@0 332 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
Chris@0 333 end
Chris@0 334 user
Chris@0 335 end
Chris@0 336 end