annotate app/models/.svn/text-base/mail_handler.rb.svn-base @ 45:65d9e2cabaa3 luisf

Added tipoftheday to the config/settings in order to correct previous issues. Tip of the day is now working correctly. Added the heading strings to the locales files.
author luisf
date Tue, 23 Nov 2010 11:50:01 +0000
parents 94944d00e43c
children af80e5618e9b 8661b858af72
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@37 20 include Redmine::I18n
Chris@0 21
Chris@0 22 class UnauthorizedAction < StandardError; end
Chris@0 23 class MissingInformation < StandardError; end
Chris@0 24
Chris@0 25 attr_reader :email, :user
Chris@0 26
Chris@0 27 def self.receive(email, options={})
Chris@0 28 @@handler_options = options.dup
Chris@0 29
Chris@0 30 @@handler_options[:issue] ||= {}
Chris@0 31
Chris@0 32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
Chris@0 33 @@handler_options[:allow_override] ||= []
Chris@0 34 # Project needs to be overridable if not specified
Chris@0 35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
Chris@0 36 # Status overridable by default
Chris@0 37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
Chris@0 38
Chris@0 39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
Chris@0 40 super email
Chris@0 41 end
Chris@0 42
Chris@0 43 # Processes incoming emails
Chris@0 44 # Returns the created object (eg. an issue, a message) or false
Chris@0 45 def receive(email)
Chris@0 46 @email = email
Chris@0 47 sender_email = email.from.to_a.first.to_s.strip
Chris@0 48 # Ignore emails received from the application emission address to avoid hell cycles
Chris@0 49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
Chris@0 50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
Chris@0 51 return false
Chris@0 52 end
Chris@0 53 @user = User.find_by_mail(sender_email) if sender_email.present?
Chris@0 54 if @user && !@user.active?
Chris@0 55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
Chris@0 56 return false
Chris@0 57 end
Chris@0 58 if @user.nil?
Chris@0 59 # Email was submitted by an unknown user
Chris@0 60 case @@handler_options[:unknown_user]
Chris@0 61 when 'accept'
Chris@0 62 @user = User.anonymous
Chris@0 63 when 'create'
Chris@0 64 @user = MailHandler.create_user_from_email(email)
Chris@0 65 if @user
Chris@0 66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
Chris@0 67 Mailer.deliver_account_information(@user, @user.password)
Chris@0 68 else
Chris@0 69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
Chris@0 70 return false
Chris@0 71 end
Chris@0 72 else
Chris@0 73 # Default behaviour, emails from unknown users are ignored
Chris@0 74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
Chris@0 75 return false
Chris@0 76 end
Chris@0 77 end
Chris@0 78 User.current = @user
Chris@0 79 dispatch
Chris@0 80 end
Chris@0 81
Chris@0 82 private
Chris@0 83
Chris@0 84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
Chris@0 85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
Chris@0 86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
Chris@0 87
Chris@0 88 def dispatch
Chris@0 89 headers = [email.in_reply_to, email.references].flatten.compact
Chris@0 90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
Chris@0 91 klass, object_id = $1, $2.to_i
Chris@0 92 method_name = "receive_#{klass}_reply"
Chris@0 93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
Chris@0 94 send method_name, object_id
Chris@0 95 else
Chris@0 96 # ignoring it
Chris@0 97 end
Chris@0 98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
Chris@0 99 receive_issue_reply(m[1].to_i)
Chris@0 100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
Chris@0 101 receive_message_reply(m[1].to_i)
Chris@0 102 else
Chris@0 103 receive_issue
Chris@0 104 end
Chris@0 105 rescue ActiveRecord::RecordInvalid => e
Chris@0 106 # TODO: send a email to the user
Chris@0 107 logger.error e.message if logger
Chris@0 108 false
Chris@0 109 rescue MissingInformation => e
Chris@0 110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
Chris@0 111 false
Chris@0 112 rescue UnauthorizedAction => e
Chris@0 113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
Chris@0 114 false
Chris@0 115 end
Chris@0 116
Chris@0 117 # Creates a new issue
Chris@0 118 def receive_issue
Chris@0 119 project = target_project
Chris@0 120 # check permission
Chris@0 121 unless @@handler_options[:no_permission_check]
Chris@0 122 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
Chris@0 123 end
Chris@0 124
chris@37 125 issue = Issue.new(:author => user, :project => project)
chris@37 126 issue.safe_attributes = issue_attributes_from_keywords(issue)
chris@37 127 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
chris@37 128 issue.subject = email.subject.to_s.chomp[0,255]
Chris@0 129 if issue.subject.blank?
Chris@0 130 issue.subject = '(no subject)'
Chris@0 131 end
Chris@0 132 issue.description = cleaned_up_text_body
chris@37 133
Chris@0 134 # add To and Cc as watchers before saving so the watchers can reply to Redmine
Chris@0 135 add_watchers(issue)
Chris@0 136 issue.save!
Chris@0 137 add_attachments(issue)
Chris@0 138 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
Chris@0 139 issue
Chris@0 140 end
Chris@0 141
Chris@0 142 # Adds a note to an existing issue
Chris@0 143 def receive_issue_reply(issue_id)
Chris@0 144 issue = Issue.find_by_id(issue_id)
Chris@0 145 return unless issue
Chris@0 146 # check permission
Chris@0 147 unless @@handler_options[:no_permission_check]
Chris@0 148 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
Chris@0 149 end
chris@37 150
Chris@0 151 journal = issue.init_journal(user, cleaned_up_text_body)
chris@37 152 issue.safe_attributes = issue_attributes_from_keywords(issue)
chris@37 153 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
Chris@0 154 add_attachments(issue)
Chris@0 155 issue.save!
Chris@0 156 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
Chris@0 157 journal
Chris@0 158 end
Chris@0 159
Chris@0 160 # Reply will be added to the issue
Chris@0 161 def receive_journal_reply(journal_id)
Chris@0 162 journal = Journal.find_by_id(journal_id)
Chris@0 163 if journal && journal.journalized_type == 'Issue'
Chris@0 164 receive_issue_reply(journal.journalized_id)
Chris@0 165 end
Chris@0 166 end
Chris@0 167
Chris@0 168 # Receives a reply to a forum message
Chris@0 169 def receive_message_reply(message_id)
Chris@0 170 message = Message.find_by_id(message_id)
Chris@0 171 if message
Chris@0 172 message = message.root
Chris@0 173
Chris@0 174 unless @@handler_options[:no_permission_check]
Chris@0 175 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
Chris@0 176 end
Chris@0 177
Chris@0 178 if !message.locked?
Chris@0 179 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
Chris@0 180 :content => cleaned_up_text_body)
Chris@0 181 reply.author = user
Chris@0 182 reply.board = message.board
Chris@0 183 message.children << reply
Chris@0 184 add_attachments(reply)
Chris@0 185 reply
Chris@0 186 else
Chris@0 187 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
Chris@0 188 end
Chris@0 189 end
Chris@0 190 end
Chris@0 191
Chris@0 192 def add_attachments(obj)
Chris@0 193 if email.has_attachments?
Chris@0 194 email.attachments.each do |attachment|
Chris@0 195 Attachment.create(:container => obj,
Chris@0 196 :file => attachment,
Chris@0 197 :author => user,
Chris@0 198 :content_type => attachment.content_type)
Chris@0 199 end
Chris@0 200 end
Chris@0 201 end
Chris@0 202
Chris@0 203 # Adds To and Cc as watchers of the given object if the sender has the
Chris@0 204 # appropriate permission
Chris@0 205 def add_watchers(obj)
Chris@0 206 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
Chris@0 207 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
Chris@0 208 unless addresses.empty?
Chris@0 209 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
Chris@0 210 watchers.each {|w| obj.add_watcher(w)}
Chris@0 211 end
Chris@0 212 end
Chris@0 213 end
Chris@0 214
Chris@0 215 def get_keyword(attr, options={})
Chris@0 216 @keywords ||= {}
Chris@0 217 if @keywords.has_key?(attr)
Chris@0 218 @keywords[attr]
Chris@0 219 else
Chris@0 220 @keywords[attr] = begin
chris@37 221 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
chris@37 222 v
Chris@0 223 elsif !@@handler_options[:issue][attr].blank?
Chris@0 224 @@handler_options[:issue][attr]
Chris@0 225 end
Chris@0 226 end
Chris@0 227 end
Chris@0 228 end
Chris@0 229
chris@37 230 # Destructively extracts the value for +attr+ in +text+
chris@37 231 # Returns nil if no matching keyword found
chris@37 232 def extract_keyword!(text, attr, format=nil)
chris@37 233 keys = [attr.to_s.humanize]
chris@37 234 if attr.is_a?(Symbol)
chris@37 235 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
chris@37 236 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
chris@37 237 end
chris@37 238 keys.reject! {|k| k.blank?}
chris@37 239 keys.collect! {|k| Regexp.escape(k)}
chris@37 240 format ||= '.+'
chris@37 241 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
chris@37 242 $2 && $2.strip
chris@37 243 end
chris@37 244
chris@37 245 def target_project
chris@37 246 # TODO: other ways to specify project:
chris@37 247 # * parse the email To field
chris@37 248 # * specific project (eg. Setting.mail_handler_target_project)
chris@37 249 target = Project.find_by_identifier(get_keyword(:project))
chris@37 250 raise MissingInformation.new('Unable to determine target project') if target.nil?
chris@37 251 target
chris@37 252 end
chris@37 253
chris@37 254 # Returns a Hash of issue attributes extracted from keywords in the email body
chris@37 255 def issue_attributes_from_keywords(issue)
chris@37 256 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
chris@37 257 assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
chris@37 258
chris@37 259 {
chris@37 260 'tracker_id' => ((k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id)) || issue.project.trackers.find(:first).try(:id),
chris@37 261 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
chris@37 262 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
chris@37 263 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
chris@37 264 'assigned_to_id' => assigned_to.try(:id),
chris@37 265 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
chris@37 266 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
chris@37 267 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
chris@37 268 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
chris@37 269 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
chris@37 270 }.delete_if {|k, v| v.blank? }
chris@37 271 end
chris@37 272
chris@37 273 # Returns a Hash of issue custom field values extracted from keywords in the email body
chris@37 274 def custom_field_values_from_keywords(customized)
chris@37 275 customized.custom_field_values.inject({}) do |h, v|
chris@37 276 if value = get_keyword(v.custom_field.name, :override => true)
chris@37 277 h[v.custom_field.id.to_s] = value
chris@37 278 end
chris@37 279 h
chris@37 280 end
chris@37 281 end
chris@37 282
Chris@0 283 # Returns the text/plain part of the email
Chris@0 284 # If not found (eg. HTML-only email), returns the body with tags removed
Chris@0 285 def plain_text_body
Chris@0 286 return @plain_text_body unless @plain_text_body.nil?
Chris@0 287 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
Chris@0 288 if parts.empty?
Chris@0 289 parts << @email
Chris@0 290 end
Chris@0 291 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
Chris@0 292 if plain_text_part.nil?
Chris@0 293 # no text/plain part found, assuming html-only email
Chris@0 294 # strip html tags and remove doctype directive
Chris@0 295 @plain_text_body = strip_tags(@email.body.to_s)
Chris@0 296 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
Chris@0 297 else
Chris@0 298 @plain_text_body = plain_text_part.body.to_s
Chris@0 299 end
Chris@0 300 @plain_text_body.strip!
Chris@0 301 @plain_text_body
Chris@0 302 end
Chris@0 303
Chris@0 304 def cleaned_up_text_body
Chris@0 305 cleanup_body(plain_text_body)
Chris@0 306 end
Chris@0 307
Chris@0 308 def self.full_sanitizer
Chris@0 309 @full_sanitizer ||= HTML::FullSanitizer.new
Chris@0 310 end
Chris@0 311
Chris@0 312 # Creates a user account for the +email+ sender
Chris@0 313 def self.create_user_from_email(email)
Chris@0 314 addr = email.from_addrs.to_a.first
Chris@0 315 if addr && !addr.spec.blank?
Chris@0 316 user = User.new
Chris@0 317 user.mail = addr.spec
Chris@0 318
Chris@0 319 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
Chris@0 320 user.firstname = names.shift
Chris@0 321 user.lastname = names.join(' ')
Chris@0 322 user.lastname = '-' if user.lastname.blank?
Chris@0 323
Chris@0 324 user.login = user.mail
Chris@0 325 user.password = ActiveSupport::SecureRandom.hex(5)
Chris@0 326 user.language = Setting.default_language
Chris@0 327 user.save ? user : nil
Chris@0 328 end
Chris@0 329 end
Chris@0 330
Chris@0 331 private
Chris@0 332
Chris@0 333 # Removes the email body of text after the truncation configurations.
Chris@0 334 def cleanup_body(body)
Chris@0 335 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
Chris@0 336 unless delimiters.empty?
chris@37 337 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
Chris@0 338 body = body.gsub(regex, '')
Chris@0 339 end
Chris@0 340 body.strip
Chris@0 341 end
Chris@0 342
Chris@0 343 def find_user_from_keyword(keyword)
Chris@0 344 user ||= User.find_by_mail(keyword)
Chris@0 345 user ||= User.find_by_login(keyword)
Chris@0 346 if user.nil? && keyword.match(/ /)
Chris@0 347 firstname, lastname = *(keyword.split) # "First Last Throwaway"
Chris@0 348 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
Chris@0 349 end
Chris@0 350 user
Chris@0 351 end
Chris@0 352 end