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