##// END OF EJS Templates
remove unneeded Relation#all from MailHandler#add_watchers...
Toshi MARUYAMA -
r12267:facf04593fe5
parent child
Show More
@@ -1,540 +1,541
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user
25 attr_reader :email, :user
26
26
27 def self.receive(email, options={})
27 def self.receive(email, options={})
28 @@handler_options = options.dup
28 @@handler_options = options.dup
29
29
30 @@handler_options[:issue] ||= {}
30 @@handler_options[:issue] ||= {}
31
31
32 if @@handler_options[:allow_override].is_a?(String)
32 if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 end
34 end
35 @@handler_options[:allow_override] ||= []
35 @@handler_options[:allow_override] ||= []
36 # Project needs to be overridable if not specified
36 # Project needs to be overridable if not specified
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 # Status overridable by default
38 # Status overridable by default
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40
40
41 @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
41 @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
42 @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
42 @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
44
44
45 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
45 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
46 super(email)
46 super(email)
47 end
47 end
48
48
49 # Extracts MailHandler options from environment variables
49 # Extracts MailHandler options from environment variables
50 # Use when receiving emails with rake tasks
50 # Use when receiving emails with rake tasks
51 def self.extract_options_from_env(env)
51 def self.extract_options_from_env(env)
52 options = {:issue => {}}
52 options = {:issue => {}}
53 %w(project status tracker category priority).each do |option|
53 %w(project status tracker category priority).each do |option|
54 options[:issue][option.to_sym] = env[option] if env[option]
54 options[:issue][option.to_sym] = env[option] if env[option]
55 end
55 end
56 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
56 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
57 options[option.to_sym] = env[option] if env[option]
57 options[option.to_sym] = env[option] if env[option]
58 end
58 end
59 options
59 options
60 end
60 end
61
61
62 def logger
62 def logger
63 Rails.logger
63 Rails.logger
64 end
64 end
65
65
66 cattr_accessor :ignored_emails_headers
66 cattr_accessor :ignored_emails_headers
67 @@ignored_emails_headers = {
67 @@ignored_emails_headers = {
68 'X-Auto-Response-Suppress' => 'oof',
68 'X-Auto-Response-Suppress' => 'oof',
69 'Auto-Submitted' => /^auto-/
69 'Auto-Submitted' => /^auto-/
70 }
70 }
71
71
72 # Processes incoming emails
72 # Processes incoming emails
73 # Returns the created object (eg. an issue, a message) or false
73 # Returns the created object (eg. an issue, a message) or false
74 def receive(email)
74 def receive(email)
75 @email = email
75 @email = email
76 sender_email = email.from.to_a.first.to_s.strip
76 sender_email = email.from.to_a.first.to_s.strip
77 # Ignore emails received from the application emission address to avoid hell cycles
77 # Ignore emails received from the application emission address to avoid hell cycles
78 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
78 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
79 if logger
79 if logger
80 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
80 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
81 end
81 end
82 return false
82 return false
83 end
83 end
84 # Ignore auto generated emails
84 # Ignore auto generated emails
85 self.class.ignored_emails_headers.each do |key, ignored_value|
85 self.class.ignored_emails_headers.each do |key, ignored_value|
86 value = email.header[key]
86 value = email.header[key]
87 if value
87 if value
88 value = value.to_s.downcase
88 value = value.to_s.downcase
89 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
89 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
90 if logger
90 if logger
91 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
91 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
92 end
92 end
93 return false
93 return false
94 end
94 end
95 end
95 end
96 end
96 end
97 @user = User.find_by_mail(sender_email) if sender_email.present?
97 @user = User.find_by_mail(sender_email) if sender_email.present?
98 if @user && !@user.active?
98 if @user && !@user.active?
99 if logger
99 if logger
100 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
100 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
101 end
101 end
102 return false
102 return false
103 end
103 end
104 if @user.nil?
104 if @user.nil?
105 # Email was submitted by an unknown user
105 # Email was submitted by an unknown user
106 case @@handler_options[:unknown_user]
106 case @@handler_options[:unknown_user]
107 when 'accept'
107 when 'accept'
108 @user = User.anonymous
108 @user = User.anonymous
109 when 'create'
109 when 'create'
110 @user = create_user_from_email
110 @user = create_user_from_email
111 if @user
111 if @user
112 if logger
112 if logger
113 logger.info "MailHandler: [#{@user.login}] account created"
113 logger.info "MailHandler: [#{@user.login}] account created"
114 end
114 end
115 add_user_to_group(@@handler_options[:default_group])
115 add_user_to_group(@@handler_options[:default_group])
116 unless @@handler_options[:no_account_notice]
116 unless @@handler_options[:no_account_notice]
117 Mailer.account_information(@user, @user.password).deliver
117 Mailer.account_information(@user, @user.password).deliver
118 end
118 end
119 else
119 else
120 if logger
120 if logger
121 logger.error "MailHandler: could not create account for [#{sender_email}]"
121 logger.error "MailHandler: could not create account for [#{sender_email}]"
122 end
122 end
123 return false
123 return false
124 end
124 end
125 else
125 else
126 # Default behaviour, emails from unknown users are ignored
126 # Default behaviour, emails from unknown users are ignored
127 if logger
127 if logger
128 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
128 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
129 end
129 end
130 return false
130 return false
131 end
131 end
132 end
132 end
133 User.current = @user
133 User.current = @user
134 dispatch
134 dispatch
135 end
135 end
136
136
137 private
137 private
138
138
139 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
139 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
140 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
140 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
141 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
141 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
142
142
143 def dispatch
143 def dispatch
144 headers = [email.in_reply_to, email.references].flatten.compact
144 headers = [email.in_reply_to, email.references].flatten.compact
145 subject = email.subject.to_s
145 subject = email.subject.to_s
146 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
146 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
147 klass, object_id = $1, $2.to_i
147 klass, object_id = $1, $2.to_i
148 method_name = "receive_#{klass}_reply"
148 method_name = "receive_#{klass}_reply"
149 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
149 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
150 send method_name, object_id
150 send method_name, object_id
151 else
151 else
152 # ignoring it
152 # ignoring it
153 end
153 end
154 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
154 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
155 receive_issue_reply(m[1].to_i)
155 receive_issue_reply(m[1].to_i)
156 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
156 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
157 receive_message_reply(m[1].to_i)
157 receive_message_reply(m[1].to_i)
158 else
158 else
159 dispatch_to_default
159 dispatch_to_default
160 end
160 end
161 rescue ActiveRecord::RecordInvalid => e
161 rescue ActiveRecord::RecordInvalid => e
162 # TODO: send a email to the user
162 # TODO: send a email to the user
163 logger.error e.message if logger
163 logger.error e.message if logger
164 false
164 false
165 rescue MissingInformation => e
165 rescue MissingInformation => e
166 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
166 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
167 false
167 false
168 rescue UnauthorizedAction => e
168 rescue UnauthorizedAction => e
169 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
169 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
170 false
170 false
171 end
171 end
172
172
173 def dispatch_to_default
173 def dispatch_to_default
174 receive_issue
174 receive_issue
175 end
175 end
176
176
177 # Creates a new issue
177 # Creates a new issue
178 def receive_issue
178 def receive_issue
179 project = target_project
179 project = target_project
180 # check permission
180 # check permission
181 unless @@handler_options[:no_permission_check]
181 unless @@handler_options[:no_permission_check]
182 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
182 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
183 end
183 end
184
184
185 issue = Issue.new(:author => user, :project => project)
185 issue = Issue.new(:author => user, :project => project)
186 issue.safe_attributes = issue_attributes_from_keywords(issue)
186 issue.safe_attributes = issue_attributes_from_keywords(issue)
187 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
187 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
188 issue.subject = cleaned_up_subject
188 issue.subject = cleaned_up_subject
189 if issue.subject.blank?
189 if issue.subject.blank?
190 issue.subject = '(no subject)'
190 issue.subject = '(no subject)'
191 end
191 end
192 issue.description = cleaned_up_text_body
192 issue.description = cleaned_up_text_body
193
193
194 # add To and Cc as watchers before saving so the watchers can reply to Redmine
194 # add To and Cc as watchers before saving so the watchers can reply to Redmine
195 add_watchers(issue)
195 add_watchers(issue)
196 issue.save!
196 issue.save!
197 add_attachments(issue)
197 add_attachments(issue)
198 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
198 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
199 issue
199 issue
200 end
200 end
201
201
202 # Adds a note to an existing issue
202 # Adds a note to an existing issue
203 def receive_issue_reply(issue_id, from_journal=nil)
203 def receive_issue_reply(issue_id, from_journal=nil)
204 issue = Issue.find_by_id(issue_id)
204 issue = Issue.find_by_id(issue_id)
205 return unless issue
205 return unless issue
206 # check permission
206 # check permission
207 unless @@handler_options[:no_permission_check]
207 unless @@handler_options[:no_permission_check]
208 unless user.allowed_to?(:add_issue_notes, issue.project) ||
208 unless user.allowed_to?(:add_issue_notes, issue.project) ||
209 user.allowed_to?(:edit_issues, issue.project)
209 user.allowed_to?(:edit_issues, issue.project)
210 raise UnauthorizedAction
210 raise UnauthorizedAction
211 end
211 end
212 end
212 end
213
213
214 # ignore CLI-supplied defaults for new issues
214 # ignore CLI-supplied defaults for new issues
215 @@handler_options[:issue].clear
215 @@handler_options[:issue].clear
216
216
217 journal = issue.init_journal(user)
217 journal = issue.init_journal(user)
218 if from_journal && from_journal.private_notes?
218 if from_journal && from_journal.private_notes?
219 # If the received email was a reply to a private note, make the added note private
219 # If the received email was a reply to a private note, make the added note private
220 issue.private_notes = true
220 issue.private_notes = true
221 end
221 end
222 issue.safe_attributes = issue_attributes_from_keywords(issue)
222 issue.safe_attributes = issue_attributes_from_keywords(issue)
223 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
223 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
224 journal.notes = cleaned_up_text_body
224 journal.notes = cleaned_up_text_body
225 add_attachments(issue)
225 add_attachments(issue)
226 issue.save!
226 issue.save!
227 if logger
227 if logger
228 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
228 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
229 end
229 end
230 journal
230 journal
231 end
231 end
232
232
233 # Reply will be added to the issue
233 # Reply will be added to the issue
234 def receive_journal_reply(journal_id)
234 def receive_journal_reply(journal_id)
235 journal = Journal.find_by_id(journal_id)
235 journal = Journal.find_by_id(journal_id)
236 if journal && journal.journalized_type == 'Issue'
236 if journal && journal.journalized_type == 'Issue'
237 receive_issue_reply(journal.journalized_id, journal)
237 receive_issue_reply(journal.journalized_id, journal)
238 end
238 end
239 end
239 end
240
240
241 # Receives a reply to a forum message
241 # Receives a reply to a forum message
242 def receive_message_reply(message_id)
242 def receive_message_reply(message_id)
243 message = Message.find_by_id(message_id)
243 message = Message.find_by_id(message_id)
244 if message
244 if message
245 message = message.root
245 message = message.root
246
246
247 unless @@handler_options[:no_permission_check]
247 unless @@handler_options[:no_permission_check]
248 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
248 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
249 end
249 end
250
250
251 if !message.locked?
251 if !message.locked?
252 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
252 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
253 :content => cleaned_up_text_body)
253 :content => cleaned_up_text_body)
254 reply.author = user
254 reply.author = user
255 reply.board = message.board
255 reply.board = message.board
256 message.children << reply
256 message.children << reply
257 add_attachments(reply)
257 add_attachments(reply)
258 reply
258 reply
259 else
259 else
260 if logger
260 if logger
261 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
261 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
262 end
262 end
263 end
263 end
264 end
264 end
265 end
265 end
266
266
267 def add_attachments(obj)
267 def add_attachments(obj)
268 if email.attachments && email.attachments.any?
268 if email.attachments && email.attachments.any?
269 email.attachments.each do |attachment|
269 email.attachments.each do |attachment|
270 next unless accept_attachment?(attachment)
270 next unless accept_attachment?(attachment)
271 obj.attachments << Attachment.create(:container => obj,
271 obj.attachments << Attachment.create(:container => obj,
272 :file => attachment.decoded,
272 :file => attachment.decoded,
273 :filename => attachment.filename,
273 :filename => attachment.filename,
274 :author => user,
274 :author => user,
275 :content_type => attachment.mime_type)
275 :content_type => attachment.mime_type)
276 end
276 end
277 end
277 end
278 end
278 end
279
279
280 # Returns false if the +attachment+ of the incoming email should be ignored
280 # Returns false if the +attachment+ of the incoming email should be ignored
281 def accept_attachment?(attachment)
281 def accept_attachment?(attachment)
282 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
282 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
283 @excluded.each do |pattern|
283 @excluded.each do |pattern|
284 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
284 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
285 if attachment.filename.to_s =~ regexp
285 if attachment.filename.to_s =~ regexp
286 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
286 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
287 return false
287 return false
288 end
288 end
289 end
289 end
290 true
290 true
291 end
291 end
292
292
293 # Adds To and Cc as watchers of the given object if the sender has the
293 # Adds To and Cc as watchers of the given object if the sender has the
294 # appropriate permission
294 # appropriate permission
295 def add_watchers(obj)
295 def add_watchers(obj)
296 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
296 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
297 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
297 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
298 unless addresses.empty?
298 unless addresses.empty?
299 watchers = User.active.where('LOWER(mail) IN (?)', addresses).all
299 User.active.where('LOWER(mail) IN (?)', addresses).each do |w|
300 watchers.each {|w| obj.add_watcher(w)}
300 obj.add_watcher(w)
301 end
301 end
302 end
302 end
303 end
303 end
304 end
304
305
305 def get_keyword(attr, options={})
306 def get_keyword(attr, options={})
306 @keywords ||= {}
307 @keywords ||= {}
307 if @keywords.has_key?(attr)
308 if @keywords.has_key?(attr)
308 @keywords[attr]
309 @keywords[attr]
309 else
310 else
310 @keywords[attr] = begin
311 @keywords[attr] = begin
311 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
312 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
312 (v = extract_keyword!(plain_text_body, attr, options[:format]))
313 (v = extract_keyword!(plain_text_body, attr, options[:format]))
313 v
314 v
314 elsif !@@handler_options[:issue][attr].blank?
315 elsif !@@handler_options[:issue][attr].blank?
315 @@handler_options[:issue][attr]
316 @@handler_options[:issue][attr]
316 end
317 end
317 end
318 end
318 end
319 end
319 end
320 end
320
321
321 # Destructively extracts the value for +attr+ in +text+
322 # Destructively extracts the value for +attr+ in +text+
322 # Returns nil if no matching keyword found
323 # Returns nil if no matching keyword found
323 def extract_keyword!(text, attr, format=nil)
324 def extract_keyword!(text, attr, format=nil)
324 keys = [attr.to_s.humanize]
325 keys = [attr.to_s.humanize]
325 if attr.is_a?(Symbol)
326 if attr.is_a?(Symbol)
326 if user && user.language.present?
327 if user && user.language.present?
327 keys << l("field_#{attr}", :default => '', :locale => user.language)
328 keys << l("field_#{attr}", :default => '', :locale => user.language)
328 end
329 end
329 if Setting.default_language.present?
330 if Setting.default_language.present?
330 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
331 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
331 end
332 end
332 end
333 end
333 keys.reject! {|k| k.blank?}
334 keys.reject! {|k| k.blank?}
334 keys.collect! {|k| Regexp.escape(k)}
335 keys.collect! {|k| Regexp.escape(k)}
335 format ||= '.+'
336 format ||= '.+'
336 keyword = nil
337 keyword = nil
337 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
338 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
338 if m = text.match(regexp)
339 if m = text.match(regexp)
339 keyword = m[2].strip
340 keyword = m[2].strip
340 text.gsub!(regexp, '')
341 text.gsub!(regexp, '')
341 end
342 end
342 keyword
343 keyword
343 end
344 end
344
345
345 def target_project
346 def target_project
346 # TODO: other ways to specify project:
347 # TODO: other ways to specify project:
347 # * parse the email To field
348 # * parse the email To field
348 # * specific project (eg. Setting.mail_handler_target_project)
349 # * specific project (eg. Setting.mail_handler_target_project)
349 target = Project.find_by_identifier(get_keyword(:project))
350 target = Project.find_by_identifier(get_keyword(:project))
350 if target.nil?
351 if target.nil?
351 # Invalid project keyword, use the project specified as the default one
352 # Invalid project keyword, use the project specified as the default one
352 default_project = @@handler_options[:issue][:project]
353 default_project = @@handler_options[:issue][:project]
353 if default_project.present?
354 if default_project.present?
354 target = Project.find_by_identifier(default_project)
355 target = Project.find_by_identifier(default_project)
355 end
356 end
356 end
357 end
357 raise MissingInformation.new('Unable to determine target project') if target.nil?
358 raise MissingInformation.new('Unable to determine target project') if target.nil?
358 target
359 target
359 end
360 end
360
361
361 # Returns a Hash of issue attributes extracted from keywords in the email body
362 # Returns a Hash of issue attributes extracted from keywords in the email body
362 def issue_attributes_from_keywords(issue)
363 def issue_attributes_from_keywords(issue)
363 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
364 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
364
365
365 attrs = {
366 attrs = {
366 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
367 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
367 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
368 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
368 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
369 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
369 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
370 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
370 'assigned_to_id' => assigned_to.try(:id),
371 'assigned_to_id' => assigned_to.try(:id),
371 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
372 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
372 issue.project.shared_versions.named(k).first.try(:id),
373 issue.project.shared_versions.named(k).first.try(:id),
373 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
374 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
374 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
375 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
375 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
376 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
376 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
377 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
377 }.delete_if {|k, v| v.blank? }
378 }.delete_if {|k, v| v.blank? }
378
379
379 if issue.new_record? && attrs['tracker_id'].nil?
380 if issue.new_record? && attrs['tracker_id'].nil?
380 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
381 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
381 end
382 end
382
383
383 attrs
384 attrs
384 end
385 end
385
386
386 # Returns a Hash of issue custom field values extracted from keywords in the email body
387 # Returns a Hash of issue custom field values extracted from keywords in the email body
387 def custom_field_values_from_keywords(customized)
388 def custom_field_values_from_keywords(customized)
388 customized.custom_field_values.inject({}) do |h, v|
389 customized.custom_field_values.inject({}) do |h, v|
389 if keyword = get_keyword(v.custom_field.name, :override => true)
390 if keyword = get_keyword(v.custom_field.name, :override => true)
390 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
391 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
391 end
392 end
392 h
393 h
393 end
394 end
394 end
395 end
395
396
396 # Returns the text/plain part of the email
397 # Returns the text/plain part of the email
397 # If not found (eg. HTML-only email), returns the body with tags removed
398 # If not found (eg. HTML-only email), returns the body with tags removed
398 def plain_text_body
399 def plain_text_body
399 return @plain_text_body unless @plain_text_body.nil?
400 return @plain_text_body unless @plain_text_body.nil?
400
401
401 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
402 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
402 text_parts
403 text_parts
403 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
404 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
404 html_parts
405 html_parts
405 else
406 else
406 [email]
407 [email]
407 end
408 end
408
409
409 parts.reject! do |part|
410 parts.reject! do |part|
410 part.header[:content_disposition].try(:disposition_type) == 'attachment'
411 part.header[:content_disposition].try(:disposition_type) == 'attachment'
411 end
412 end
412
413
413 @plain_text_body = parts.map do |p|
414 @plain_text_body = parts.map do |p|
414 body_charset = p.charset.respond_to?(:force_encoding) ?
415 body_charset = p.charset.respond_to?(:force_encoding) ?
415 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
416 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
416 Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
417 Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
417 end.join("\r\n")
418 end.join("\r\n")
418
419
419 # strip html tags and remove doctype directive
420 # strip html tags and remove doctype directive
420 if parts.any? {|p| p.mime_type == 'text/html'}
421 if parts.any? {|p| p.mime_type == 'text/html'}
421 @plain_text_body = strip_tags(@plain_text_body.strip)
422 @plain_text_body = strip_tags(@plain_text_body.strip)
422 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
423 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
423 end
424 end
424
425
425 @plain_text_body
426 @plain_text_body
426 end
427 end
427
428
428 def cleaned_up_text_body
429 def cleaned_up_text_body
429 cleanup_body(plain_text_body)
430 cleanup_body(plain_text_body)
430 end
431 end
431
432
432 def cleaned_up_subject
433 def cleaned_up_subject
433 subject = email.subject.to_s
434 subject = email.subject.to_s
434 subject.strip[0,255]
435 subject.strip[0,255]
435 end
436 end
436
437
437 def self.full_sanitizer
438 def self.full_sanitizer
438 @full_sanitizer ||= HTML::FullSanitizer.new
439 @full_sanitizer ||= HTML::FullSanitizer.new
439 end
440 end
440
441
441 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
442 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
442 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
443 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
443 value = value.to_s.slice(0, limit)
444 value = value.to_s.slice(0, limit)
444 object.send("#{attribute}=", value)
445 object.send("#{attribute}=", value)
445 end
446 end
446
447
447 # Returns a User from an email address and a full name
448 # Returns a User from an email address and a full name
448 def self.new_user_from_attributes(email_address, fullname=nil)
449 def self.new_user_from_attributes(email_address, fullname=nil)
449 user = User.new
450 user = User.new
450
451
451 # Truncating the email address would result in an invalid format
452 # Truncating the email address would result in an invalid format
452 user.mail = email_address
453 user.mail = email_address
453 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
454 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
454
455
455 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
456 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
456 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
457 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
457 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
458 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
458 user.lastname = '-' if user.lastname.blank?
459 user.lastname = '-' if user.lastname.blank?
459 user.language = Setting.default_language
460 user.language = Setting.default_language
460 user.generate_password = true
461 user.generate_password = true
461 user.mail_notification = 'only_my_events'
462 user.mail_notification = 'only_my_events'
462
463
463 unless user.valid?
464 unless user.valid?
464 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
465 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
465 user.firstname = "-" unless user.errors[:firstname].blank?
466 user.firstname = "-" unless user.errors[:firstname].blank?
466 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
467 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
467 end
468 end
468
469
469 user
470 user
470 end
471 end
471
472
472 # Creates a User for the +email+ sender
473 # Creates a User for the +email+ sender
473 # Returns the user or nil if it could not be created
474 # Returns the user or nil if it could not be created
474 def create_user_from_email
475 def create_user_from_email
475 from = email.header['from'].to_s
476 from = email.header['from'].to_s
476 addr, name = from, nil
477 addr, name = from, nil
477 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
478 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
478 addr, name = m[2], m[1]
479 addr, name = m[2], m[1]
479 end
480 end
480 if addr.present?
481 if addr.present?
481 user = self.class.new_user_from_attributes(addr, name)
482 user = self.class.new_user_from_attributes(addr, name)
482 if @@handler_options[:no_notification]
483 if @@handler_options[:no_notification]
483 user.mail_notification = 'none'
484 user.mail_notification = 'none'
484 end
485 end
485 if user.save
486 if user.save
486 user
487 user
487 else
488 else
488 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
489 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
489 nil
490 nil
490 end
491 end
491 else
492 else
492 logger.error "MailHandler: failed to create User: no FROM address found" if logger
493 logger.error "MailHandler: failed to create User: no FROM address found" if logger
493 nil
494 nil
494 end
495 end
495 end
496 end
496
497
497 # Adds the newly created user to default group
498 # Adds the newly created user to default group
498 def add_user_to_group(default_group)
499 def add_user_to_group(default_group)
499 if default_group.present?
500 if default_group.present?
500 default_group.split(',').each do |group_name|
501 default_group.split(',').each do |group_name|
501 if group = Group.named(group_name).first
502 if group = Group.named(group_name).first
502 group.users << @user
503 group.users << @user
503 elsif logger
504 elsif logger
504 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
505 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
505 end
506 end
506 end
507 end
507 end
508 end
508 end
509 end
509
510
510 # Removes the email body of text after the truncation configurations.
511 # Removes the email body of text after the truncation configurations.
511 def cleanup_body(body)
512 def cleanup_body(body)
512 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
513 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
513 unless delimiters.empty?
514 unless delimiters.empty?
514 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
515 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
515 body = body.gsub(regex, '')
516 body = body.gsub(regex, '')
516 end
517 end
517 body.strip
518 body.strip
518 end
519 end
519
520
520 def find_assignee_from_keyword(keyword, issue)
521 def find_assignee_from_keyword(keyword, issue)
521 keyword = keyword.to_s.downcase
522 keyword = keyword.to_s.downcase
522 assignable = issue.assignable_users
523 assignable = issue.assignable_users
523 assignee = nil
524 assignee = nil
524 assignee ||= assignable.detect {|a|
525 assignee ||= assignable.detect {|a|
525 a.mail.to_s.downcase == keyword ||
526 a.mail.to_s.downcase == keyword ||
526 a.login.to_s.downcase == keyword
527 a.login.to_s.downcase == keyword
527 }
528 }
528 if assignee.nil? && keyword.match(/ /)
529 if assignee.nil? && keyword.match(/ /)
529 firstname, lastname = *(keyword.split) # "First Last Throwaway"
530 firstname, lastname = *(keyword.split) # "First Last Throwaway"
530 assignee ||= assignable.detect {|a|
531 assignee ||= assignable.detect {|a|
531 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
532 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
532 a.lastname.to_s.downcase == lastname
533 a.lastname.to_s.downcase == lastname
533 }
534 }
534 end
535 end
535 if assignee.nil?
536 if assignee.nil?
536 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
537 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
537 end
538 end
538 assignee
539 assignee
539 end
540 end
540 end
541 end
General Comments 0
You need to be logged in to leave comments. Login now