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