##// END OF EJS Templates
Email handler: set a default issue subject if the email subject is blank (#3850)....
Jean-Philippe Lang -
r2759:275b555b0974
parent child
Show More
@@ -1,280 +1,283
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.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.toutf8
124 if issue.subject.blank?
125 issue.subject = '(no subject)'
126 end
124 issue.description = plain_text_body
127 issue.description = plain_text_body
125 # custom fields
128 # custom fields
126 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
129 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
127 if value = get_keyword(c.name, :override => true)
130 if value = get_keyword(c.name, :override => true)
128 h[c.id] = value
131 h[c.id] = value
129 end
132 end
130 h
133 h
131 end
134 end
132 # add To and Cc as watchers before saving so the watchers can reply to Redmine
135 # add To and Cc as watchers before saving so the watchers can reply to Redmine
133 add_watchers(issue)
136 add_watchers(issue)
134 issue.save!
137 issue.save!
135 add_attachments(issue)
138 add_attachments(issue)
136 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
139 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
137 issue
140 issue
138 end
141 end
139
142
140 def target_project
143 def target_project
141 # TODO: other ways to specify project:
144 # TODO: other ways to specify project:
142 # * parse the email To field
145 # * parse the email To field
143 # * specific project (eg. Setting.mail_handler_target_project)
146 # * specific project (eg. Setting.mail_handler_target_project)
144 target = Project.find_by_identifier(get_keyword(:project))
147 target = Project.find_by_identifier(get_keyword(:project))
145 raise MissingInformation.new('Unable to determine target project') if target.nil?
148 raise MissingInformation.new('Unable to determine target project') if target.nil?
146 target
149 target
147 end
150 end
148
151
149 # Adds a note to an existing issue
152 # Adds a note to an existing issue
150 def receive_issue_reply(issue_id)
153 def receive_issue_reply(issue_id)
151 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
154 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
152
155
153 issue = Issue.find_by_id(issue_id)
156 issue = Issue.find_by_id(issue_id)
154 return unless issue
157 return unless issue
155 # check permission
158 # check permission
156 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
159 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
157 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
160 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
158
161
159 # add the note
162 # add the note
160 journal = issue.init_journal(user, plain_text_body)
163 journal = issue.init_journal(user, plain_text_body)
161 add_attachments(issue)
164 add_attachments(issue)
162 # check workflow
165 # check workflow
163 if status && issue.new_statuses_allowed_to(user).include?(status)
166 if status && issue.new_statuses_allowed_to(user).include?(status)
164 issue.status = status
167 issue.status = status
165 end
168 end
166 issue.save!
169 issue.save!
167 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
170 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
168 journal
171 journal
169 end
172 end
170
173
171 # Reply will be added to the issue
174 # Reply will be added to the issue
172 def receive_journal_reply(journal_id)
175 def receive_journal_reply(journal_id)
173 journal = Journal.find_by_id(journal_id)
176 journal = Journal.find_by_id(journal_id)
174 if journal && journal.journalized_type == 'Issue'
177 if journal && journal.journalized_type == 'Issue'
175 receive_issue_reply(journal.journalized_id)
178 receive_issue_reply(journal.journalized_id)
176 end
179 end
177 end
180 end
178
181
179 # Receives a reply to a forum message
182 # Receives a reply to a forum message
180 def receive_message_reply(message_id)
183 def receive_message_reply(message_id)
181 message = Message.find_by_id(message_id)
184 message = Message.find_by_id(message_id)
182 if message
185 if message
183 message = message.root
186 message = message.root
184 if user.allowed_to?(:add_messages, message.project) && !message.locked?
187 if user.allowed_to?(:add_messages, message.project) && !message.locked?
185 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
188 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
186 :content => plain_text_body)
189 :content => plain_text_body)
187 reply.author = user
190 reply.author = user
188 reply.board = message.board
191 reply.board = message.board
189 message.children << reply
192 message.children << reply
190 add_attachments(reply)
193 add_attachments(reply)
191 reply
194 reply
192 else
195 else
193 raise UnauthorizedAction
196 raise UnauthorizedAction
194 end
197 end
195 end
198 end
196 end
199 end
197
200
198 def add_attachments(obj)
201 def add_attachments(obj)
199 if email.has_attachments?
202 if email.has_attachments?
200 email.attachments.each do |attachment|
203 email.attachments.each do |attachment|
201 Attachment.create(:container => obj,
204 Attachment.create(:container => obj,
202 :file => attachment,
205 :file => attachment,
203 :author => user,
206 :author => user,
204 :content_type => attachment.content_type)
207 :content_type => attachment.content_type)
205 end
208 end
206 end
209 end
207 end
210 end
208
211
209 # Adds To and Cc as watchers of the given object if the sender has the
212 # Adds To and Cc as watchers of the given object if the sender has the
210 # appropriate permission
213 # appropriate permission
211 def add_watchers(obj)
214 def add_watchers(obj)
212 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
215 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
213 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
216 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
214 unless addresses.empty?
217 unless addresses.empty?
215 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
218 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
216 watchers.each {|w| obj.add_watcher(w)}
219 watchers.each {|w| obj.add_watcher(w)}
217 end
220 end
218 end
221 end
219 end
222 end
220
223
221 def get_keyword(attr, options={})
224 def get_keyword(attr, options={})
222 @keywords ||= {}
225 @keywords ||= {}
223 if @keywords.has_key?(attr)
226 if @keywords.has_key?(attr)
224 @keywords[attr]
227 @keywords[attr]
225 else
228 else
226 @keywords[attr] = begin
229 @keywords[attr] = begin
227 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
230 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
228 $1.strip
231 $1.strip
229 elsif !@@handler_options[:issue][attr].blank?
232 elsif !@@handler_options[:issue][attr].blank?
230 @@handler_options[:issue][attr]
233 @@handler_options[:issue][attr]
231 end
234 end
232 end
235 end
233 end
236 end
234 end
237 end
235
238
236 # Returns the text/plain part of the email
239 # Returns the text/plain part of the email
237 # If not found (eg. HTML-only email), returns the body with tags removed
240 # If not found (eg. HTML-only email), returns the body with tags removed
238 def plain_text_body
241 def plain_text_body
239 return @plain_text_body unless @plain_text_body.nil?
242 return @plain_text_body unless @plain_text_body.nil?
240 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
243 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
241 if parts.empty?
244 if parts.empty?
242 parts << @email
245 parts << @email
243 end
246 end
244 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
247 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
245 if plain_text_part.nil?
248 if plain_text_part.nil?
246 # no text/plain part found, assuming html-only email
249 # no text/plain part found, assuming html-only email
247 # strip html tags and remove doctype directive
250 # strip html tags and remove doctype directive
248 @plain_text_body = strip_tags(@email.body.to_s)
251 @plain_text_body = strip_tags(@email.body.to_s)
249 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
252 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
250 else
253 else
251 @plain_text_body = plain_text_part.body.to_s
254 @plain_text_body = plain_text_part.body.to_s
252 end
255 end
253 @plain_text_body.strip!
256 @plain_text_body.strip!
254 @plain_text_body
257 @plain_text_body
255 end
258 end
256
259
257
260
258 def self.full_sanitizer
261 def self.full_sanitizer
259 @full_sanitizer ||= HTML::FullSanitizer.new
262 @full_sanitizer ||= HTML::FullSanitizer.new
260 end
263 end
261
264
262 # Creates a user account for the +email+ sender
265 # Creates a user account for the +email+ sender
263 def self.create_user_from_email(email)
266 def self.create_user_from_email(email)
264 addr = email.from_addrs.to_a.first
267 addr = email.from_addrs.to_a.first
265 if addr && !addr.spec.blank?
268 if addr && !addr.spec.blank?
266 user = User.new
269 user = User.new
267 user.mail = addr.spec
270 user.mail = addr.spec
268
271
269 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
272 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
270 user.firstname = names.shift
273 user.firstname = names.shift
271 user.lastname = names.join(' ')
274 user.lastname = names.join(' ')
272 user.lastname = '-' if user.lastname.blank?
275 user.lastname = '-' if user.lastname.blank?
273
276
274 user.login = user.mail
277 user.login = user.mail
275 user.password = ActiveSupport::SecureRandom.hex(5)
278 user.password = ActiveSupport::SecureRandom.hex(5)
276 user.language = Setting.default_language
279 user.language = Setting.default_language
277 user.save ? user : nil
280 user.save ? user : nil
278 end
281 end
279 end
282 end
280 end
283 end
General Comments 0
You need to be logged in to leave comments. Login now