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