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