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