##// END OF EJS Templates
Fixes MailHandler for ruby1.9....
Jean-Philippe Lang -
r2801:d8ba5c2a069b
parent child
Show More
@@ -1,283 +1,284
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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
20
21 class UnauthorizedAction < StandardError; end
21 class UnauthorizedAction < StandardError; end
22 class MissingInformation < StandardError; end
22 class MissingInformation < StandardError; end
23
23
24 attr_reader :email, :user
24 attr_reader :email, :user
25
25
26 def self.receive(email, options={})
26 def self.receive(email, options={})
27 @@handler_options = options.dup
27 @@handler_options = options.dup
28
28
29 @@handler_options[:issue] ||= {}
29 @@handler_options[:issue] ||= {}
30
30
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 @@handler_options[:allow_override] ||= []
32 @@handler_options[:allow_override] ||= []
33 # Project needs to be overridable if not specified
33 # Project needs to be overridable if not specified
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 # Status overridable by default
35 # Status overridable by default
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 super email
37 super email
38 end
38 end
39
39
40 # Processes incoming emails
40 # Processes incoming emails
41 # Returns the created object (eg. an issue, a message) or false
41 # Returns the created object (eg. an issue, a message) or false
42 def receive(email)
42 def receive(email)
43 @email = email
43 @email = email
44 @user = User.find_by_mail(email.from.to_a.first.to_s.strip)
44 @user = User.find_by_mail(email.from.to_a.first.to_s.strip)
45 if @user && !@user.active?
45 if @user && !@user.active?
46 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
46 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
47 return false
47 return false
48 end
48 end
49 if @user.nil?
49 if @user.nil?
50 # Email was submitted by an unknown user
50 # Email was submitted by an unknown user
51 case @@handler_options[:unknown_user]
51 case @@handler_options[:unknown_user]
52 when 'accept'
52 when 'accept'
53 @user = User.anonymous
53 @user = User.anonymous
54 when 'create'
54 when 'create'
55 @user = MailHandler.create_user_from_email(email)
55 @user = MailHandler.create_user_from_email(email)
56 if @user
56 if @user
57 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
57 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
58 Mailer.deliver_account_information(@user, @user.password)
58 Mailer.deliver_account_information(@user, @user.password)
59 else
59 else
60 logger.error "MailHandler: could not create account for [#{email.from.first}]" if logger && logger.error
60 logger.error "MailHandler: could not create account for [#{email.from.first}]" if logger && logger.error
61 return false
61 return false
62 end
62 end
63 else
63 else
64 # Default behaviour, emails from unknown users are ignored
64 # Default behaviour, emails from unknown users are ignored
65 logger.info "MailHandler: ignoring email from unknown user [#{email.from.first}]" if logger && logger.info
65 logger.info "MailHandler: ignoring email from unknown user [#{email.from.first}]" if logger && logger.info
66 return false
66 return false
67 end
67 end
68 end
68 end
69 User.current = @user
69 User.current = @user
70 dispatch
70 dispatch
71 end
71 end
72
72
73 private
73 private
74
74
75 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
75 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
76 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
76 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
77 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
77 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
78
78
79 def dispatch
79 def dispatch
80 headers = [email.in_reply_to, email.references].flatten.compact
80 headers = [email.in_reply_to, email.references].flatten.compact
81 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
81 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
82 klass, object_id = $1, $2.to_i
82 klass, object_id = $1, $2.to_i
83 method_name = "receive_#{klass}_reply"
83 method_name = "receive_#{klass}_reply"
84 if self.class.private_instance_methods.include?(method_name)
84 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
85 send method_name, object_id
85 send method_name, object_id
86 else
86 else
87 # ignoring it
87 # ignoring it
88 end
88 end
89 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
89 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
90 receive_issue_reply(m[1].to_i)
90 receive_issue_reply(m[1].to_i)
91 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
91 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
92 receive_message_reply(m[1].to_i)
92 receive_message_reply(m[1].to_i)
93 else
93 else
94 receive_issue
94 receive_issue
95 end
95 end
96 rescue ActiveRecord::RecordInvalid => e
96 rescue ActiveRecord::RecordInvalid => e
97 # TODO: send a email to the user
97 # TODO: send a email to the user
98 logger.error e.message if logger
98 logger.error e.message if logger
99 false
99 false
100 rescue MissingInformation => e
100 rescue MissingInformation => e
101 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
101 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
102 false
102 false
103 rescue UnauthorizedAction => e
103 rescue UnauthorizedAction => e
104 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
104 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
105 false
105 false
106 end
106 end
107
107
108 # Creates a new issue
108 # Creates a new issue
109 def receive_issue
109 def receive_issue
110 project = target_project
110 project = target_project
111 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
111 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
112 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
112 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
113 priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
113 priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
114 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
114 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
115
115
116 # check permission
116 # check permission
117 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
117 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
118 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
118 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
119 # check workflow
119 # check workflow
120 if status && issue.new_statuses_allowed_to(user).include?(status)
120 if status && issue.new_statuses_allowed_to(user).include?(status)
121 issue.status = status
121 issue.status = status
122 end
122 end
123 issue.subject = email.subject.chomp.toutf8
123 issue.subject = email.subject.chomp
124 issue.subject = issue.subject.toutf8 if issue.subject.respond_to?(:toutf8)
124 if issue.subject.blank?
125 if issue.subject.blank?
125 issue.subject = '(no subject)'
126 issue.subject = '(no subject)'
126 end
127 end
127 issue.description = plain_text_body
128 issue.description = plain_text_body
128 # custom fields
129 # custom fields
129 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
130 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
130 if value = get_keyword(c.name, :override => true)
131 if value = get_keyword(c.name, :override => true)
131 h[c.id] = value
132 h[c.id] = value
132 end
133 end
133 h
134 h
134 end
135 end
135 # add To and Cc as watchers before saving so the watchers can reply to Redmine
136 # add To and Cc as watchers before saving so the watchers can reply to Redmine
136 add_watchers(issue)
137 add_watchers(issue)
137 issue.save!
138 issue.save!
138 add_attachments(issue)
139 add_attachments(issue)
139 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
140 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
140 issue
141 issue
141 end
142 end
142
143
143 def target_project
144 def target_project
144 # TODO: other ways to specify project:
145 # TODO: other ways to specify project:
145 # * parse the email To field
146 # * parse the email To field
146 # * specific project (eg. Setting.mail_handler_target_project)
147 # * specific project (eg. Setting.mail_handler_target_project)
147 target = Project.find_by_identifier(get_keyword(:project))
148 target = Project.find_by_identifier(get_keyword(:project))
148 raise MissingInformation.new('Unable to determine target project') if target.nil?
149 raise MissingInformation.new('Unable to determine target project') if target.nil?
149 target
150 target
150 end
151 end
151
152
152 # Adds a note to an existing issue
153 # Adds a note to an existing issue
153 def receive_issue_reply(issue_id)
154 def receive_issue_reply(issue_id)
154 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
155 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
155
156
156 issue = Issue.find_by_id(issue_id)
157 issue = Issue.find_by_id(issue_id)
157 return unless issue
158 return unless issue
158 # check permission
159 # check permission
159 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
160 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
160 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
161 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
161
162
162 # add the note
163 # add the note
163 journal = issue.init_journal(user, plain_text_body)
164 journal = issue.init_journal(user, plain_text_body)
164 add_attachments(issue)
165 add_attachments(issue)
165 # check workflow
166 # check workflow
166 if status && issue.new_statuses_allowed_to(user).include?(status)
167 if status && issue.new_statuses_allowed_to(user).include?(status)
167 issue.status = status
168 issue.status = status
168 end
169 end
169 issue.save!
170 issue.save!
170 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
171 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
171 journal
172 journal
172 end
173 end
173
174
174 # Reply will be added to the issue
175 # Reply will be added to the issue
175 def receive_journal_reply(journal_id)
176 def receive_journal_reply(journal_id)
176 journal = Journal.find_by_id(journal_id)
177 journal = Journal.find_by_id(journal_id)
177 if journal && journal.journalized_type == 'Issue'
178 if journal && journal.journalized_type == 'Issue'
178 receive_issue_reply(journal.journalized_id)
179 receive_issue_reply(journal.journalized_id)
179 end
180 end
180 end
181 end
181
182
182 # Receives a reply to a forum message
183 # Receives a reply to a forum message
183 def receive_message_reply(message_id)
184 def receive_message_reply(message_id)
184 message = Message.find_by_id(message_id)
185 message = Message.find_by_id(message_id)
185 if message
186 if message
186 message = message.root
187 message = message.root
187 if user.allowed_to?(:add_messages, message.project) && !message.locked?
188 if user.allowed_to?(:add_messages, message.project) && !message.locked?
188 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
189 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
189 :content => plain_text_body)
190 :content => plain_text_body)
190 reply.author = user
191 reply.author = user
191 reply.board = message.board
192 reply.board = message.board
192 message.children << reply
193 message.children << reply
193 add_attachments(reply)
194 add_attachments(reply)
194 reply
195 reply
195 else
196 else
196 raise UnauthorizedAction
197 raise UnauthorizedAction
197 end
198 end
198 end
199 end
199 end
200 end
200
201
201 def add_attachments(obj)
202 def add_attachments(obj)
202 if email.has_attachments?
203 if email.has_attachments?
203 email.attachments.each do |attachment|
204 email.attachments.each do |attachment|
204 Attachment.create(:container => obj,
205 Attachment.create(:container => obj,
205 :file => attachment,
206 :file => attachment,
206 :author => user,
207 :author => user,
207 :content_type => attachment.content_type)
208 :content_type => attachment.content_type)
208 end
209 end
209 end
210 end
210 end
211 end
211
212
212 # Adds To and Cc as watchers of the given object if the sender has the
213 # Adds To and Cc as watchers of the given object if the sender has the
213 # appropriate permission
214 # appropriate permission
214 def add_watchers(obj)
215 def add_watchers(obj)
215 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
216 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
216 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
217 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
217 unless addresses.empty?
218 unless addresses.empty?
218 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
219 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
219 watchers.each {|w| obj.add_watcher(w)}
220 watchers.each {|w| obj.add_watcher(w)}
220 end
221 end
221 end
222 end
222 end
223 end
223
224
224 def get_keyword(attr, options={})
225 def get_keyword(attr, options={})
225 @keywords ||= {}
226 @keywords ||= {}
226 if @keywords.has_key?(attr)
227 if @keywords.has_key?(attr)
227 @keywords[attr]
228 @keywords[attr]
228 else
229 else
229 @keywords[attr] = begin
230 @keywords[attr] = begin
230 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
231 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
231 $1.strip
232 $1.strip
232 elsif !@@handler_options[:issue][attr].blank?
233 elsif !@@handler_options[:issue][attr].blank?
233 @@handler_options[:issue][attr]
234 @@handler_options[:issue][attr]
234 end
235 end
235 end
236 end
236 end
237 end
237 end
238 end
238
239
239 # Returns the text/plain part of the email
240 # Returns the text/plain part of the email
240 # If not found (eg. HTML-only email), returns the body with tags removed
241 # If not found (eg. HTML-only email), returns the body with tags removed
241 def plain_text_body
242 def plain_text_body
242 return @plain_text_body unless @plain_text_body.nil?
243 return @plain_text_body unless @plain_text_body.nil?
243 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
244 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
244 if parts.empty?
245 if parts.empty?
245 parts << @email
246 parts << @email
246 end
247 end
247 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
248 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
248 if plain_text_part.nil?
249 if plain_text_part.nil?
249 # no text/plain part found, assuming html-only email
250 # no text/plain part found, assuming html-only email
250 # strip html tags and remove doctype directive
251 # strip html tags and remove doctype directive
251 @plain_text_body = strip_tags(@email.body.to_s)
252 @plain_text_body = strip_tags(@email.body.to_s)
252 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
253 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
253 else
254 else
254 @plain_text_body = plain_text_part.body.to_s
255 @plain_text_body = plain_text_part.body.to_s
255 end
256 end
256 @plain_text_body.strip!
257 @plain_text_body.strip!
257 @plain_text_body
258 @plain_text_body
258 end
259 end
259
260
260
261
261 def self.full_sanitizer
262 def self.full_sanitizer
262 @full_sanitizer ||= HTML::FullSanitizer.new
263 @full_sanitizer ||= HTML::FullSanitizer.new
263 end
264 end
264
265
265 # Creates a user account for the +email+ sender
266 # Creates a user account for the +email+ sender
266 def self.create_user_from_email(email)
267 def self.create_user_from_email(email)
267 addr = email.from_addrs.to_a.first
268 addr = email.from_addrs.to_a.first
268 if addr && !addr.spec.blank?
269 if addr && !addr.spec.blank?
269 user = User.new
270 user = User.new
270 user.mail = addr.spec
271 user.mail = addr.spec
271
272
272 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
273 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
273 user.firstname = names.shift
274 user.firstname = names.shift
274 user.lastname = names.join(' ')
275 user.lastname = names.join(' ')
275 user.lastname = '-' if user.lastname.blank?
276 user.lastname = '-' if user.lastname.blank?
276
277
277 user.login = user.mail
278 user.login = user.mail
278 user.password = ActiveSupport::SecureRandom.hex(5)
279 user.password = ActiveSupport::SecureRandom.hex(5)
279 user.language = Setting.default_language
280 user.language = Setting.default_language
280 user.save ? user : nil
281 user.save ? user : nil
281 end
282 end
282 end
283 end
283 end
284 end
General Comments 0
You need to be logged in to leave comments. Login now