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