##// END OF EJS Templates
Makes MailHandler ignore invalid keyword values to avoid validation failures....
Jean-Philippe Lang -
r4282:abf988ad69cc
parent child
Show More
@@ -0,0 +1,46
1 Return-Path: <jsmith@somenet.foo>
2 Received: from osiris ([127.0.0.1])
3 by OSIRIS
4 with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
5 Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
6 From: "John Smith" <jsmith@somenet.foo>
7 To: <redmine@somenet.foo>
8 Subject: New ticket on a given project
9 Date: Sun, 22 Jun 2008 12:28:07 +0200
10 MIME-Version: 1.0
11 Content-Type: text/plain;
12 format=flowed;
13 charset="iso-8859-1";
14 reply-type=original
15 Content-Transfer-Encoding: 7bit
16 X-Priority: 3
17 X-MSMail-Priority: Normal
18 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
19 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
20
21 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
22 turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
23 blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
24 sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
25 in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
26 sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
27 id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
28 eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
29 sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
30 malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
31 platea dictumst.
32
33 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
34 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
35 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
36 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
37 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
38 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
39
40 Project: onlinestore
41 Tracker: Feature request
42 category: Stock management
43 priority: foo
44 done ratio: x
45 start date: some day
46 due date: never
@@ -1,348 +1,349
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 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user
25 attr_reader :email, :user
26
26
27 def self.receive(email, options={})
27 def self.receive(email, options={})
28 @@handler_options = options.dup
28 @@handler_options = options.dup
29
29
30 @@handler_options[:issue] ||= {}
30 @@handler_options[:issue] ||= {}
31
31
32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] ||= []
33 @@handler_options[:allow_override] ||= []
34 # Project needs to be overridable if not specified
34 # Project needs to be overridable if not specified
35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
36 # Status overridable by default
36 # Status overridable by default
37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
38
38
39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
40 super email
40 super email
41 end
41 end
42
42
43 # Processes incoming emails
43 # Processes incoming emails
44 # Returns the created object (eg. an issue, a message) or false
44 # Returns the created object (eg. an issue, a message) or false
45 def receive(email)
45 def receive(email)
46 @email = email
46 @email = email
47 sender_email = email.from.to_a.first.to_s.strip
47 sender_email = email.from.to_a.first.to_s.strip
48 # Ignore emails received from the application emission address to avoid hell cycles
48 # Ignore emails received from the application emission address to avoid hell cycles
49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
51 return false
51 return false
52 end
52 end
53 @user = User.find_by_mail(sender_email) if sender_email.present?
53 @user = User.find_by_mail(sender_email) if sender_email.present?
54 if @user && !@user.active?
54 if @user && !@user.active?
55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
56 return false
56 return false
57 end
57 end
58 if @user.nil?
58 if @user.nil?
59 # Email was submitted by an unknown user
59 # Email was submitted by an unknown user
60 case @@handler_options[:unknown_user]
60 case @@handler_options[:unknown_user]
61 when 'accept'
61 when 'accept'
62 @user = User.anonymous
62 @user = User.anonymous
63 when 'create'
63 when 'create'
64 @user = MailHandler.create_user_from_email(email)
64 @user = MailHandler.create_user_from_email(email)
65 if @user
65 if @user
66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
67 Mailer.deliver_account_information(@user, @user.password)
67 Mailer.deliver_account_information(@user, @user.password)
68 else
68 else
69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
70 return false
70 return false
71 end
71 end
72 else
72 else
73 # Default behaviour, emails from unknown users are ignored
73 # Default behaviour, emails from unknown users are ignored
74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
75 return false
75 return false
76 end
76 end
77 end
77 end
78 User.current = @user
78 User.current = @user
79 dispatch
79 dispatch
80 end
80 end
81
81
82 private
82 private
83
83
84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
87
87
88 def dispatch
88 def dispatch
89 headers = [email.in_reply_to, email.references].flatten.compact
89 headers = [email.in_reply_to, email.references].flatten.compact
90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
91 klass, object_id = $1, $2.to_i
91 klass, object_id = $1, $2.to_i
92 method_name = "receive_#{klass}_reply"
92 method_name = "receive_#{klass}_reply"
93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
94 send method_name, object_id
94 send method_name, object_id
95 else
95 else
96 # ignoring it
96 # ignoring it
97 end
97 end
98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
99 receive_issue_reply(m[1].to_i)
99 receive_issue_reply(m[1].to_i)
100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
101 receive_message_reply(m[1].to_i)
101 receive_message_reply(m[1].to_i)
102 else
102 else
103 receive_issue
103 receive_issue
104 end
104 end
105 rescue ActiveRecord::RecordInvalid => e
105 rescue ActiveRecord::RecordInvalid => e
106 # TODO: send a email to the user
106 # TODO: send a email to the user
107 logger.error e.message if logger
107 logger.error e.message if logger
108 false
108 false
109 rescue MissingInformation => e
109 rescue MissingInformation => e
110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
111 false
111 false
112 rescue UnauthorizedAction => e
112 rescue UnauthorizedAction => e
113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
114 false
114 false
115 end
115 end
116
116
117 # Creates a new issue
117 # Creates a new issue
118 def receive_issue
118 def receive_issue
119 project = target_project
119 project = target_project
120 # check permission
120 # check permission
121 unless @@handler_options[:no_permission_check]
121 unless @@handler_options[:no_permission_check]
122 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
122 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
123 end
123 end
124
124
125 issue = Issue.new(:author => user, :project => project)
125 issue = Issue.new(:author => user, :project => project)
126 issue.safe_attributes = issue_attributes_from_keywords(issue)
126 issue.safe_attributes = issue_attributes_from_keywords(issue)
127 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
127 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
128 issue.subject = email.subject.to_s.chomp[0,255]
128 issue.subject = email.subject.to_s.chomp[0,255]
129 if issue.subject.blank?
129 if issue.subject.blank?
130 issue.subject = '(no subject)'
130 issue.subject = '(no subject)'
131 end
131 end
132 issue.description = cleaned_up_text_body
132 issue.description = cleaned_up_text_body
133
133
134 # add To and Cc as watchers before saving so the watchers can reply to Redmine
134 # add To and Cc as watchers before saving so the watchers can reply to Redmine
135 add_watchers(issue)
135 add_watchers(issue)
136 issue.save!
136 issue.save!
137 add_attachments(issue)
137 add_attachments(issue)
138 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
138 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
139 issue
139 issue
140 end
140 end
141
141
142 # Adds a note to an existing issue
142 # Adds a note to an existing issue
143 def receive_issue_reply(issue_id)
143 def receive_issue_reply(issue_id)
144 issue = Issue.find_by_id(issue_id)
144 issue = Issue.find_by_id(issue_id)
145 return unless issue
145 return unless issue
146 # check permission
146 # check permission
147 unless @@handler_options[:no_permission_check]
147 unless @@handler_options[:no_permission_check]
148 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
148 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
149 end
149 end
150
150
151 journal = issue.init_journal(user, cleaned_up_text_body)
151 journal = issue.init_journal(user, cleaned_up_text_body)
152 issue.safe_attributes = issue_attributes_from_keywords(issue)
152 issue.safe_attributes = issue_attributes_from_keywords(issue)
153 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
153 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
154 add_attachments(issue)
154 add_attachments(issue)
155 issue.save!
155 issue.save!
156 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
156 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
157 journal
157 journal
158 end
158 end
159
159
160 # Reply will be added to the issue
160 # Reply will be added to the issue
161 def receive_journal_reply(journal_id)
161 def receive_journal_reply(journal_id)
162 journal = Journal.find_by_id(journal_id)
162 journal = Journal.find_by_id(journal_id)
163 if journal && journal.journalized_type == 'Issue'
163 if journal && journal.journalized_type == 'Issue'
164 receive_issue_reply(journal.journalized_id)
164 receive_issue_reply(journal.journalized_id)
165 end
165 end
166 end
166 end
167
167
168 # Receives a reply to a forum message
168 # Receives a reply to a forum message
169 def receive_message_reply(message_id)
169 def receive_message_reply(message_id)
170 message = Message.find_by_id(message_id)
170 message = Message.find_by_id(message_id)
171 if message
171 if message
172 message = message.root
172 message = message.root
173
173
174 unless @@handler_options[:no_permission_check]
174 unless @@handler_options[:no_permission_check]
175 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
175 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
176 end
176 end
177
177
178 if !message.locked?
178 if !message.locked?
179 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
179 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
180 :content => cleaned_up_text_body)
180 :content => cleaned_up_text_body)
181 reply.author = user
181 reply.author = user
182 reply.board = message.board
182 reply.board = message.board
183 message.children << reply
183 message.children << reply
184 add_attachments(reply)
184 add_attachments(reply)
185 reply
185 reply
186 else
186 else
187 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
187 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
188 end
188 end
189 end
189 end
190 end
190 end
191
191
192 def add_attachments(obj)
192 def add_attachments(obj)
193 if email.has_attachments?
193 if email.has_attachments?
194 email.attachments.each do |attachment|
194 email.attachments.each do |attachment|
195 Attachment.create(:container => obj,
195 Attachment.create(:container => obj,
196 :file => attachment,
196 :file => attachment,
197 :author => user,
197 :author => user,
198 :content_type => attachment.content_type)
198 :content_type => attachment.content_type)
199 end
199 end
200 end
200 end
201 end
201 end
202
202
203 # Adds To and Cc as watchers of the given object if the sender has the
203 # Adds To and Cc as watchers of the given object if the sender has the
204 # appropriate permission
204 # appropriate permission
205 def add_watchers(obj)
205 def add_watchers(obj)
206 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
206 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
207 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
207 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
208 unless addresses.empty?
208 unless addresses.empty?
209 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
209 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
210 watchers.each {|w| obj.add_watcher(w)}
210 watchers.each {|w| obj.add_watcher(w)}
211 end
211 end
212 end
212 end
213 end
213 end
214
214
215 def get_keyword(attr, options={})
215 def get_keyword(attr, options={})
216 @keywords ||= {}
216 @keywords ||= {}
217 if @keywords.has_key?(attr)
217 if @keywords.has_key?(attr)
218 @keywords[attr]
218 @keywords[attr]
219 else
219 else
220 @keywords[attr] = begin
220 @keywords[attr] = begin
221 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr))
221 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
222 v
222 v
223 elsif !@@handler_options[:issue][attr].blank?
223 elsif !@@handler_options[:issue][attr].blank?
224 @@handler_options[:issue][attr]
224 @@handler_options[:issue][attr]
225 end
225 end
226 end
226 end
227 end
227 end
228 end
228 end
229
229
230 # Destructively extracts the value for +attr+ in +text+
230 # Destructively extracts the value for +attr+ in +text+
231 # Returns nil if no matching keyword found
231 # Returns nil if no matching keyword found
232 def extract_keyword!(text, attr)
232 def extract_keyword!(text, attr, format=nil)
233 keys = [attr.to_s.humanize]
233 keys = [attr.to_s.humanize]
234 if attr.is_a?(Symbol)
234 if attr.is_a?(Symbol)
235 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
235 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
236 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
236 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
237 end
237 end
238 keys.reject! {|k| k.blank?}
238 keys.reject! {|k| k.blank?}
239 keys.collect! {|k| Regexp.escape(k)}
239 keys.collect! {|k| Regexp.escape(k)}
240 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(.+)\s*$/i, '')
240 format ||= '.+'
241 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
241 $2 && $2.strip
242 $2 && $2.strip
242 end
243 end
243
244
244 def target_project
245 def target_project
245 # TODO: other ways to specify project:
246 # TODO: other ways to specify project:
246 # * parse the email To field
247 # * parse the email To field
247 # * specific project (eg. Setting.mail_handler_target_project)
248 # * specific project (eg. Setting.mail_handler_target_project)
248 target = Project.find_by_identifier(get_keyword(:project))
249 target = Project.find_by_identifier(get_keyword(:project))
249 raise MissingInformation.new('Unable to determine target project') if target.nil?
250 raise MissingInformation.new('Unable to determine target project') if target.nil?
250 target
251 target
251 end
252 end
252
253
253 # Returns a Hash of issue attributes extracted from keywords in the email body
254 # Returns a Hash of issue attributes extracted from keywords in the email body
254 def issue_attributes_from_keywords(issue)
255 def issue_attributes_from_keywords(issue)
255 {
256 {
256 'tracker_id' => ((k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id)) || issue.project.trackers.find(:first).try(:id),
257 'tracker_id' => ((k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id)) || issue.project.trackers.find(:first).try(:id),
257 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
258 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
258 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
259 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
259 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
260 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
260 'assigned_to_id' => (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k).try(:id),
261 'assigned_to_id' => (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k).try(:id),
261 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
262 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
262 'start_date' => get_keyword(:start_date, :override => true),
263 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
263 'due_date' => get_keyword(:due_date, :override => true),
264 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
264 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
265 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
265 'done_ratio' => get_keyword(:done_ratio, :override => true),
266 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
266 }.delete_if {|k, v| v.blank? }
267 }.delete_if {|k, v| v.blank? }
267 end
268 end
268
269
269 # Returns a Hash of issue custom field values extracted from keywords in the email body
270 # Returns a Hash of issue custom field values extracted from keywords in the email body
270 def custom_field_values_from_keywords(customized)
271 def custom_field_values_from_keywords(customized)
271 customized.custom_field_values.inject({}) do |h, v|
272 customized.custom_field_values.inject({}) do |h, v|
272 if value = get_keyword(v.custom_field.name, :override => true)
273 if value = get_keyword(v.custom_field.name, :override => true)
273 h[v.custom_field.id.to_s] = value
274 h[v.custom_field.id.to_s] = value
274 end
275 end
275 h
276 h
276 end
277 end
277 end
278 end
278
279
279 # Returns the text/plain part of the email
280 # Returns the text/plain part of the email
280 # If not found (eg. HTML-only email), returns the body with tags removed
281 # If not found (eg. HTML-only email), returns the body with tags removed
281 def plain_text_body
282 def plain_text_body
282 return @plain_text_body unless @plain_text_body.nil?
283 return @plain_text_body unless @plain_text_body.nil?
283 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
284 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
284 if parts.empty?
285 if parts.empty?
285 parts << @email
286 parts << @email
286 end
287 end
287 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
288 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
288 if plain_text_part.nil?
289 if plain_text_part.nil?
289 # no text/plain part found, assuming html-only email
290 # no text/plain part found, assuming html-only email
290 # strip html tags and remove doctype directive
291 # strip html tags and remove doctype directive
291 @plain_text_body = strip_tags(@email.body.to_s)
292 @plain_text_body = strip_tags(@email.body.to_s)
292 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
293 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
293 else
294 else
294 @plain_text_body = plain_text_part.body.to_s
295 @plain_text_body = plain_text_part.body.to_s
295 end
296 end
296 @plain_text_body.strip!
297 @plain_text_body.strip!
297 @plain_text_body
298 @plain_text_body
298 end
299 end
299
300
300 def cleaned_up_text_body
301 def cleaned_up_text_body
301 cleanup_body(plain_text_body)
302 cleanup_body(plain_text_body)
302 end
303 end
303
304
304 def self.full_sanitizer
305 def self.full_sanitizer
305 @full_sanitizer ||= HTML::FullSanitizer.new
306 @full_sanitizer ||= HTML::FullSanitizer.new
306 end
307 end
307
308
308 # Creates a user account for the +email+ sender
309 # Creates a user account for the +email+ sender
309 def self.create_user_from_email(email)
310 def self.create_user_from_email(email)
310 addr = email.from_addrs.to_a.first
311 addr = email.from_addrs.to_a.first
311 if addr && !addr.spec.blank?
312 if addr && !addr.spec.blank?
312 user = User.new
313 user = User.new
313 user.mail = addr.spec
314 user.mail = addr.spec
314
315
315 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
316 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
316 user.firstname = names.shift
317 user.firstname = names.shift
317 user.lastname = names.join(' ')
318 user.lastname = names.join(' ')
318 user.lastname = '-' if user.lastname.blank?
319 user.lastname = '-' if user.lastname.blank?
319
320
320 user.login = user.mail
321 user.login = user.mail
321 user.password = ActiveSupport::SecureRandom.hex(5)
322 user.password = ActiveSupport::SecureRandom.hex(5)
322 user.language = Setting.default_language
323 user.language = Setting.default_language
323 user.save ? user : nil
324 user.save ? user : nil
324 end
325 end
325 end
326 end
326
327
327 private
328 private
328
329
329 # Removes the email body of text after the truncation configurations.
330 # Removes the email body of text after the truncation configurations.
330 def cleanup_body(body)
331 def cleanup_body(body)
331 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
332 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
332 unless delimiters.empty?
333 unless delimiters.empty?
333 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
334 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
334 body = body.gsub(regex, '')
335 body = body.gsub(regex, '')
335 end
336 end
336 body.strip
337 body.strip
337 end
338 end
338
339
339 def find_user_from_keyword(keyword)
340 def find_user_from_keyword(keyword)
340 user ||= User.find_by_mail(keyword)
341 user ||= User.find_by_mail(keyword)
341 user ||= User.find_by_login(keyword)
342 user ||= User.find_by_login(keyword)
342 if user.nil? && keyword.match(/ /)
343 if user.nil? && keyword.match(/ /)
343 firstname, lastname = *(keyword.split) # "First Last Throwaway"
344 firstname, lastname = *(keyword.split) # "First Last Throwaway"
344 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
345 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
345 end
346 end
346 user
347 user
347 end
348 end
348 end
349 end
@@ -1,425 +1,437
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2009 Jean-Philippe Lang
4 # Copyright (C) 2006-2009 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.dirname(__FILE__) + '/../test_helper'
20 require File.dirname(__FILE__) + '/../test_helper'
21
21
22 class MailHandlerTest < ActiveSupport::TestCase
22 class MailHandlerTest < ActiveSupport::TestCase
23 fixtures :users, :projects,
23 fixtures :users, :projects,
24 :enabled_modules,
24 :enabled_modules,
25 :roles,
25 :roles,
26 :members,
26 :members,
27 :member_roles,
27 :member_roles,
28 :issues,
28 :issues,
29 :issue_statuses,
29 :issue_statuses,
30 :workflows,
30 :workflows,
31 :trackers,
31 :trackers,
32 :projects_trackers,
32 :projects_trackers,
33 :versions,
33 :versions,
34 :enumerations,
34 :enumerations,
35 :issue_categories,
35 :issue_categories,
36 :custom_fields,
36 :custom_fields,
37 :custom_fields_trackers,
37 :custom_fields_trackers,
38 :boards,
38 :boards,
39 :messages
39 :messages
40
40
41 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
41 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
42
42
43 def setup
43 def setup
44 ActionMailer::Base.deliveries.clear
44 ActionMailer::Base.deliveries.clear
45 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
45 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
46 end
46 end
47
47
48 def test_add_issue
48 def test_add_issue
49 ActionMailer::Base.deliveries.clear
49 ActionMailer::Base.deliveries.clear
50 # This email contains: 'Project: onlinestore'
50 # This email contains: 'Project: onlinestore'
51 issue = submit_email('ticket_on_given_project.eml')
51 issue = submit_email('ticket_on_given_project.eml')
52 assert issue.is_a?(Issue)
52 assert issue.is_a?(Issue)
53 assert !issue.new_record?
53 assert !issue.new_record?
54 issue.reload
54 issue.reload
55 assert_equal 'New ticket on a given project', issue.subject
55 assert_equal 'New ticket on a given project', issue.subject
56 assert_equal User.find_by_login('jsmith'), issue.author
56 assert_equal User.find_by_login('jsmith'), issue.author
57 assert_equal Project.find(2), issue.project
57 assert_equal Project.find(2), issue.project
58 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
58 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
59 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
59 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
60 assert_equal '2010-01-01', issue.start_date.to_s
60 assert_equal '2010-01-01', issue.start_date.to_s
61 assert_equal '2010-12-31', issue.due_date.to_s
61 assert_equal '2010-12-31', issue.due_date.to_s
62 assert_equal User.find_by_login('jsmith'), issue.assigned_to
62 assert_equal User.find_by_login('jsmith'), issue.assigned_to
63 assert_equal Version.find_by_name('alpha'), issue.fixed_version
63 assert_equal Version.find_by_name('alpha'), issue.fixed_version
64 assert_equal 2.5, issue.estimated_hours
64 assert_equal 2.5, issue.estimated_hours
65 assert_equal 30, issue.done_ratio
65 assert_equal 30, issue.done_ratio
66 # keywords should be removed from the email body
66 # keywords should be removed from the email body
67 assert !issue.description.match(/^Project:/i)
67 assert !issue.description.match(/^Project:/i)
68 assert !issue.description.match(/^Status:/i)
68 assert !issue.description.match(/^Status:/i)
69 # Email notification should be sent
69 # Email notification should be sent
70 mail = ActionMailer::Base.deliveries.last
70 mail = ActionMailer::Base.deliveries.last
71 assert_not_nil mail
71 assert_not_nil mail
72 assert mail.subject.include?('New ticket on a given project')
72 assert mail.subject.include?('New ticket on a given project')
73 end
73 end
74
74
75 def test_add_issue_with_status
75 def test_add_issue_with_status
76 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
76 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
77 issue = submit_email('ticket_on_given_project.eml')
77 issue = submit_email('ticket_on_given_project.eml')
78 assert issue.is_a?(Issue)
78 assert issue.is_a?(Issue)
79 assert !issue.new_record?
79 assert !issue.new_record?
80 issue.reload
80 issue.reload
81 assert_equal Project.find(2), issue.project
81 assert_equal Project.find(2), issue.project
82 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
82 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
83 end
83 end
84
84
85 def test_add_issue_with_attributes_override
85 def test_add_issue_with_attributes_override
86 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
86 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
87 assert issue.is_a?(Issue)
87 assert issue.is_a?(Issue)
88 assert !issue.new_record?
88 assert !issue.new_record?
89 issue.reload
89 issue.reload
90 assert_equal 'New ticket on a given project', issue.subject
90 assert_equal 'New ticket on a given project', issue.subject
91 assert_equal User.find_by_login('jsmith'), issue.author
91 assert_equal User.find_by_login('jsmith'), issue.author
92 assert_equal Project.find(2), issue.project
92 assert_equal Project.find(2), issue.project
93 assert_equal 'Feature request', issue.tracker.to_s
93 assert_equal 'Feature request', issue.tracker.to_s
94 assert_equal 'Stock management', issue.category.to_s
94 assert_equal 'Stock management', issue.category.to_s
95 assert_equal 'Urgent', issue.priority.to_s
95 assert_equal 'Urgent', issue.priority.to_s
96 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
96 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
97 end
97 end
98
98
99 def test_add_issue_with_partial_attributes_override
99 def test_add_issue_with_partial_attributes_override
100 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
100 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
101 assert issue.is_a?(Issue)
101 assert issue.is_a?(Issue)
102 assert !issue.new_record?
102 assert !issue.new_record?
103 issue.reload
103 issue.reload
104 assert_equal 'New ticket on a given project', issue.subject
104 assert_equal 'New ticket on a given project', issue.subject
105 assert_equal User.find_by_login('jsmith'), issue.author
105 assert_equal User.find_by_login('jsmith'), issue.author
106 assert_equal Project.find(2), issue.project
106 assert_equal Project.find(2), issue.project
107 assert_equal 'Feature request', issue.tracker.to_s
107 assert_equal 'Feature request', issue.tracker.to_s
108 assert_nil issue.category
108 assert_nil issue.category
109 assert_equal 'High', issue.priority.to_s
109 assert_equal 'High', issue.priority.to_s
110 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
110 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
111 end
111 end
112
112
113 def test_add_issue_with_spaces_between_attribute_and_separator
113 def test_add_issue_with_spaces_between_attribute_and_separator
114 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
114 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
115 assert issue.is_a?(Issue)
115 assert issue.is_a?(Issue)
116 assert !issue.new_record?
116 assert !issue.new_record?
117 issue.reload
117 issue.reload
118 assert_equal 'New ticket on a given project', issue.subject
118 assert_equal 'New ticket on a given project', issue.subject
119 assert_equal User.find_by_login('jsmith'), issue.author
119 assert_equal User.find_by_login('jsmith'), issue.author
120 assert_equal Project.find(2), issue.project
120 assert_equal Project.find(2), issue.project
121 assert_equal 'Feature request', issue.tracker.to_s
121 assert_equal 'Feature request', issue.tracker.to_s
122 assert_equal 'Stock management', issue.category.to_s
122 assert_equal 'Stock management', issue.category.to_s
123 assert_equal 'Urgent', issue.priority.to_s
123 assert_equal 'Urgent', issue.priority.to_s
124 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
124 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
125 end
125 end
126
126
127
127
128 def test_add_issue_with_attachment_to_specific_project
128 def test_add_issue_with_attachment_to_specific_project
129 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
129 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
130 assert issue.is_a?(Issue)
130 assert issue.is_a?(Issue)
131 assert !issue.new_record?
131 assert !issue.new_record?
132 issue.reload
132 issue.reload
133 assert_equal 'Ticket created by email with attachment', issue.subject
133 assert_equal 'Ticket created by email with attachment', issue.subject
134 assert_equal User.find_by_login('jsmith'), issue.author
134 assert_equal User.find_by_login('jsmith'), issue.author
135 assert_equal Project.find(2), issue.project
135 assert_equal Project.find(2), issue.project
136 assert_equal 'This is a new ticket with attachments', issue.description
136 assert_equal 'This is a new ticket with attachments', issue.description
137 # Attachment properties
137 # Attachment properties
138 assert_equal 1, issue.attachments.size
138 assert_equal 1, issue.attachments.size
139 assert_equal 'Paella.jpg', issue.attachments.first.filename
139 assert_equal 'Paella.jpg', issue.attachments.first.filename
140 assert_equal 'image/jpeg', issue.attachments.first.content_type
140 assert_equal 'image/jpeg', issue.attachments.first.content_type
141 assert_equal 10790, issue.attachments.first.filesize
141 assert_equal 10790, issue.attachments.first.filesize
142 end
142 end
143
143
144 def test_add_issue_with_custom_fields
144 def test_add_issue_with_custom_fields
145 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
145 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
146 assert issue.is_a?(Issue)
146 assert issue.is_a?(Issue)
147 assert !issue.new_record?
147 assert !issue.new_record?
148 issue.reload
148 issue.reload
149 assert_equal 'New ticket with custom field values', issue.subject
149 assert_equal 'New ticket with custom field values', issue.subject
150 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
150 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
151 assert !issue.description.match(/^searchable field:/i)
151 assert !issue.description.match(/^searchable field:/i)
152 end
152 end
153
153
154 def test_add_issue_with_cc
154 def test_add_issue_with_cc
155 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
155 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
156 assert issue.is_a?(Issue)
156 assert issue.is_a?(Issue)
157 assert !issue.new_record?
157 assert !issue.new_record?
158 issue.reload
158 issue.reload
159 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
159 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
160 assert_equal 1, issue.watcher_user_ids.size
160 assert_equal 1, issue.watcher_user_ids.size
161 end
161 end
162
162
163 def test_add_issue_by_unknown_user
163 def test_add_issue_by_unknown_user
164 assert_no_difference 'User.count' do
164 assert_no_difference 'User.count' do
165 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
165 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
166 end
166 end
167 end
167 end
168
168
169 def test_add_issue_by_anonymous_user
169 def test_add_issue_by_anonymous_user
170 Role.anonymous.add_permission!(:add_issues)
170 Role.anonymous.add_permission!(:add_issues)
171 assert_no_difference 'User.count' do
171 assert_no_difference 'User.count' do
172 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
172 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
173 assert issue.is_a?(Issue)
173 assert issue.is_a?(Issue)
174 assert issue.author.anonymous?
174 assert issue.author.anonymous?
175 end
175 end
176 end
176 end
177
177
178 def test_add_issue_by_anonymous_user_with_no_from_address
178 def test_add_issue_by_anonymous_user_with_no_from_address
179 Role.anonymous.add_permission!(:add_issues)
179 Role.anonymous.add_permission!(:add_issues)
180 assert_no_difference 'User.count' do
180 assert_no_difference 'User.count' do
181 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
181 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
182 assert issue.is_a?(Issue)
182 assert issue.is_a?(Issue)
183 assert issue.author.anonymous?
183 assert issue.author.anonymous?
184 end
184 end
185 end
185 end
186
186
187 def test_add_issue_by_anonymous_user_on_private_project
187 def test_add_issue_by_anonymous_user_on_private_project
188 Role.anonymous.add_permission!(:add_issues)
188 Role.anonymous.add_permission!(:add_issues)
189 assert_no_difference 'User.count' do
189 assert_no_difference 'User.count' do
190 assert_no_difference 'Issue.count' do
190 assert_no_difference 'Issue.count' do
191 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
191 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
192 end
192 end
193 end
193 end
194 end
194 end
195
195
196 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
196 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
197 assert_no_difference 'User.count' do
197 assert_no_difference 'User.count' do
198 assert_difference 'Issue.count' do
198 assert_difference 'Issue.count' do
199 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
199 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
200 assert issue.is_a?(Issue)
200 assert issue.is_a?(Issue)
201 assert issue.author.anonymous?
201 assert issue.author.anonymous?
202 assert !issue.project.is_public?
202 assert !issue.project.is_public?
203 end
203 end
204 end
204 end
205 end
205 end
206
206
207 def test_add_issue_by_created_user
207 def test_add_issue_by_created_user
208 Setting.default_language = 'en'
208 Setting.default_language = 'en'
209 assert_difference 'User.count' do
209 assert_difference 'User.count' do
210 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
210 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
211 assert issue.is_a?(Issue)
211 assert issue.is_a?(Issue)
212 assert issue.author.active?
212 assert issue.author.active?
213 assert_equal 'john.doe@somenet.foo', issue.author.mail
213 assert_equal 'john.doe@somenet.foo', issue.author.mail
214 assert_equal 'John', issue.author.firstname
214 assert_equal 'John', issue.author.firstname
215 assert_equal 'Doe', issue.author.lastname
215 assert_equal 'Doe', issue.author.lastname
216
216
217 # account information
217 # account information
218 email = ActionMailer::Base.deliveries.first
218 email = ActionMailer::Base.deliveries.first
219 assert_not_nil email
219 assert_not_nil email
220 assert email.subject.include?('account activation')
220 assert email.subject.include?('account activation')
221 login = email.body.match(/\* Login: (.*)$/)[1]
221 login = email.body.match(/\* Login: (.*)$/)[1]
222 password = email.body.match(/\* Password: (.*)$/)[1]
222 password = email.body.match(/\* Password: (.*)$/)[1]
223 assert_equal issue.author, User.try_to_login(login, password)
223 assert_equal issue.author, User.try_to_login(login, password)
224 end
224 end
225 end
225 end
226
226
227 def test_add_issue_without_from_header
227 def test_add_issue_without_from_header
228 Role.anonymous.add_permission!(:add_issues)
228 Role.anonymous.add_permission!(:add_issues)
229 assert_equal false, submit_email('ticket_without_from_header.eml')
229 assert_equal false, submit_email('ticket_without_from_header.eml')
230 end
230 end
231
231
232 def test_add_issue_with_invalid_attributes
233 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
234 assert issue.is_a?(Issue)
235 assert !issue.new_record?
236 issue.reload
237 assert_nil issue.start_date
238 assert_nil issue.due_date
239 assert_equal 0, issue.done_ratio
240 assert_equal 'Normal', issue.priority.to_s
241 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
242 end
243
232 def test_add_issue_with_localized_attributes
244 def test_add_issue_with_localized_attributes
233 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
245 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
234 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
246 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
235 assert issue.is_a?(Issue)
247 assert issue.is_a?(Issue)
236 assert !issue.new_record?
248 assert !issue.new_record?
237 issue.reload
249 issue.reload
238 assert_equal 'New ticket on a given project', issue.subject
250 assert_equal 'New ticket on a given project', issue.subject
239 assert_equal User.find_by_login('jsmith'), issue.author
251 assert_equal User.find_by_login('jsmith'), issue.author
240 assert_equal Project.find(2), issue.project
252 assert_equal Project.find(2), issue.project
241 assert_equal 'Feature request', issue.tracker.to_s
253 assert_equal 'Feature request', issue.tracker.to_s
242 assert_equal 'Stock management', issue.category.to_s
254 assert_equal 'Stock management', issue.category.to_s
243 assert_equal 'Urgent', issue.priority.to_s
255 assert_equal 'Urgent', issue.priority.to_s
244 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
256 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
245 end
257 end
246
258
247 def test_add_issue_with_japanese_keywords
259 def test_add_issue_with_japanese_keywords
248 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
260 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
249 Project.find(1).trackers << tracker
261 Project.find(1).trackers << tracker
250 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
262 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
251 assert_kind_of Issue, issue
263 assert_kind_of Issue, issue
252 assert_equal tracker, issue.tracker
264 assert_equal tracker, issue.tracker
253 end
265 end
254
266
255 def test_should_ignore_emails_from_emission_address
267 def test_should_ignore_emails_from_emission_address
256 Role.anonymous.add_permission!(:add_issues)
268 Role.anonymous.add_permission!(:add_issues)
257 assert_no_difference 'User.count' do
269 assert_no_difference 'User.count' do
258 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
270 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
259 end
271 end
260 end
272 end
261
273
262 def test_add_issue_should_send_email_notification
274 def test_add_issue_should_send_email_notification
263 Setting.notified_events = ['issue_added']
275 Setting.notified_events = ['issue_added']
264 ActionMailer::Base.deliveries.clear
276 ActionMailer::Base.deliveries.clear
265 # This email contains: 'Project: onlinestore'
277 # This email contains: 'Project: onlinestore'
266 issue = submit_email('ticket_on_given_project.eml')
278 issue = submit_email('ticket_on_given_project.eml')
267 assert issue.is_a?(Issue)
279 assert issue.is_a?(Issue)
268 assert_equal 1, ActionMailer::Base.deliveries.size
280 assert_equal 1, ActionMailer::Base.deliveries.size
269 end
281 end
270
282
271 def test_add_issue_note
283 def test_add_issue_note
272 journal = submit_email('ticket_reply.eml')
284 journal = submit_email('ticket_reply.eml')
273 assert journal.is_a?(Journal)
285 assert journal.is_a?(Journal)
274 assert_equal User.find_by_login('jsmith'), journal.user
286 assert_equal User.find_by_login('jsmith'), journal.user
275 assert_equal Issue.find(2), journal.journalized
287 assert_equal Issue.find(2), journal.journalized
276 assert_match /This is reply/, journal.notes
288 assert_match /This is reply/, journal.notes
277 end
289 end
278
290
279 def test_add_issue_note_with_attribute_changes
291 def test_add_issue_note_with_attribute_changes
280 # This email contains: 'Status: Resolved'
292 # This email contains: 'Status: Resolved'
281 journal = submit_email('ticket_reply_with_status.eml')
293 journal = submit_email('ticket_reply_with_status.eml')
282 assert journal.is_a?(Journal)
294 assert journal.is_a?(Journal)
283 issue = Issue.find(journal.issue.id)
295 issue = Issue.find(journal.issue.id)
284 assert_equal User.find_by_login('jsmith'), journal.user
296 assert_equal User.find_by_login('jsmith'), journal.user
285 assert_equal Issue.find(2), journal.journalized
297 assert_equal Issue.find(2), journal.journalized
286 assert_match /This is reply/, journal.notes
298 assert_match /This is reply/, journal.notes
287 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
299 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
288 assert_equal '2010-01-01', issue.start_date.to_s
300 assert_equal '2010-01-01', issue.start_date.to_s
289 assert_equal '2010-12-31', issue.due_date.to_s
301 assert_equal '2010-12-31', issue.due_date.to_s
290 assert_equal User.find_by_login('jsmith'), issue.assigned_to
302 assert_equal User.find_by_login('jsmith'), issue.assigned_to
291 assert_equal 'Updated custom value', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
303 assert_equal 'Updated custom value', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
292 end
304 end
293
305
294 def test_add_issue_note_should_send_email_notification
306 def test_add_issue_note_should_send_email_notification
295 ActionMailer::Base.deliveries.clear
307 ActionMailer::Base.deliveries.clear
296 journal = submit_email('ticket_reply.eml')
308 journal = submit_email('ticket_reply.eml')
297 assert journal.is_a?(Journal)
309 assert journal.is_a?(Journal)
298 assert_equal 1, ActionMailer::Base.deliveries.size
310 assert_equal 1, ActionMailer::Base.deliveries.size
299 end
311 end
300
312
301 def test_reply_to_a_message
313 def test_reply_to_a_message
302 m = submit_email('message_reply.eml')
314 m = submit_email('message_reply.eml')
303 assert m.is_a?(Message)
315 assert m.is_a?(Message)
304 assert !m.new_record?
316 assert !m.new_record?
305 m.reload
317 m.reload
306 assert_equal 'Reply via email', m.subject
318 assert_equal 'Reply via email', m.subject
307 # The email replies to message #2 which is part of the thread of message #1
319 # The email replies to message #2 which is part of the thread of message #1
308 assert_equal Message.find(1), m.parent
320 assert_equal Message.find(1), m.parent
309 end
321 end
310
322
311 def test_reply_to_a_message_by_subject
323 def test_reply_to_a_message_by_subject
312 m = submit_email('message_reply_by_subject.eml')
324 m = submit_email('message_reply_by_subject.eml')
313 assert m.is_a?(Message)
325 assert m.is_a?(Message)
314 assert !m.new_record?
326 assert !m.new_record?
315 m.reload
327 m.reload
316 assert_equal 'Reply to the first post', m.subject
328 assert_equal 'Reply to the first post', m.subject
317 assert_equal Message.find(1), m.parent
329 assert_equal Message.find(1), m.parent
318 end
330 end
319
331
320 def test_should_strip_tags_of_html_only_emails
332 def test_should_strip_tags_of_html_only_emails
321 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
333 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
322 assert issue.is_a?(Issue)
334 assert issue.is_a?(Issue)
323 assert !issue.new_record?
335 assert !issue.new_record?
324 issue.reload
336 issue.reload
325 assert_equal 'HTML email', issue.subject
337 assert_equal 'HTML email', issue.subject
326 assert_equal 'This is a html-only email.', issue.description
338 assert_equal 'This is a html-only email.', issue.description
327 end
339 end
328
340
329 context "truncate emails based on the Setting" do
341 context "truncate emails based on the Setting" do
330 context "with no setting" do
342 context "with no setting" do
331 setup do
343 setup do
332 Setting.mail_handler_body_delimiters = ''
344 Setting.mail_handler_body_delimiters = ''
333 end
345 end
334
346
335 should "add the entire email into the issue" do
347 should "add the entire email into the issue" do
336 issue = submit_email('ticket_on_given_project.eml')
348 issue = submit_email('ticket_on_given_project.eml')
337 assert_issue_created(issue)
349 assert_issue_created(issue)
338 assert issue.description.include?('---')
350 assert issue.description.include?('---')
339 assert issue.description.include?('This paragraph is after the delimiter')
351 assert issue.description.include?('This paragraph is after the delimiter')
340 end
352 end
341 end
353 end
342
354
343 context "with a single string" do
355 context "with a single string" do
344 setup do
356 setup do
345 Setting.mail_handler_body_delimiters = '---'
357 Setting.mail_handler_body_delimiters = '---'
346 end
358 end
347
359
348 should "truncate the email at the delimiter for the issue" do
360 should "truncate the email at the delimiter for the issue" do
349 issue = submit_email('ticket_on_given_project.eml')
361 issue = submit_email('ticket_on_given_project.eml')
350 assert_issue_created(issue)
362 assert_issue_created(issue)
351 assert issue.description.include?('This paragraph is before delimiters')
363 assert issue.description.include?('This paragraph is before delimiters')
352 assert issue.description.include?('--- This line starts with a delimiter')
364 assert issue.description.include?('--- This line starts with a delimiter')
353 assert !issue.description.match(/^---$/)
365 assert !issue.description.match(/^---$/)
354 assert !issue.description.include?('This paragraph is after the delimiter')
366 assert !issue.description.include?('This paragraph is after the delimiter')
355 end
367 end
356 end
368 end
357
369
358 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
370 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
359 setup do
371 setup do
360 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
372 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
361 end
373 end
362
374
363 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
375 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
364 journal = submit_email('issue_update_with_quoted_reply_above.eml')
376 journal = submit_email('issue_update_with_quoted_reply_above.eml')
365 assert journal.is_a?(Journal)
377 assert journal.is_a?(Journal)
366 assert journal.notes.include?('An update to the issue by the sender.')
378 assert journal.notes.include?('An update to the issue by the sender.')
367 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
379 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
368 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
380 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
369
381
370 end
382 end
371
383
372 end
384 end
373
385
374 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
386 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
375 setup do
387 setup do
376 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
388 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
377 end
389 end
378
390
379 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
391 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
380 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
392 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
381 assert journal.is_a?(Journal)
393 assert journal.is_a?(Journal)
382 assert journal.notes.include?('An update to the issue by the sender.')
394 assert journal.notes.include?('An update to the issue by the sender.')
383 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
395 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
384 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
396 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
385
397
386 end
398 end
387
399
388 end
400 end
389
401
390 context "with multiple strings" do
402 context "with multiple strings" do
391 setup do
403 setup do
392 Setting.mail_handler_body_delimiters = "---\nBREAK"
404 Setting.mail_handler_body_delimiters = "---\nBREAK"
393 end
405 end
394
406
395 should "truncate the email at the first delimiter found (BREAK)" do
407 should "truncate the email at the first delimiter found (BREAK)" do
396 issue = submit_email('ticket_on_given_project.eml')
408 issue = submit_email('ticket_on_given_project.eml')
397 assert_issue_created(issue)
409 assert_issue_created(issue)
398 assert issue.description.include?('This paragraph is before delimiters')
410 assert issue.description.include?('This paragraph is before delimiters')
399 assert !issue.description.include?('BREAK')
411 assert !issue.description.include?('BREAK')
400 assert !issue.description.include?('This paragraph is between delimiters')
412 assert !issue.description.include?('This paragraph is between delimiters')
401 assert !issue.description.match(/^---$/)
413 assert !issue.description.match(/^---$/)
402 assert !issue.description.include?('This paragraph is after the delimiter')
414 assert !issue.description.include?('This paragraph is after the delimiter')
403 end
415 end
404 end
416 end
405 end
417 end
406
418
407 def test_email_with_long_subject_line
419 def test_email_with_long_subject_line
408 issue = submit_email('ticket_with_long_subject.eml')
420 issue = submit_email('ticket_with_long_subject.eml')
409 assert issue.is_a?(Issue)
421 assert issue.is_a?(Issue)
410 assert_equal issue.subject, 'New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say...'[0,255]
422 assert_equal issue.subject, 'New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say...'[0,255]
411 end
423 end
412
424
413 private
425 private
414
426
415 def submit_email(filename, options={})
427 def submit_email(filename, options={})
416 raw = IO.read(File.join(FIXTURES_PATH, filename))
428 raw = IO.read(File.join(FIXTURES_PATH, filename))
417 MailHandler.receive(raw, options)
429 MailHandler.receive(raw, options)
418 end
430 end
419
431
420 def assert_issue_created(issue)
432 def assert_issue_created(issue)
421 assert issue.is_a?(Issue)
433 assert issue.is_a?(Issue)
422 assert !issue.new_record?
434 assert !issue.new_record?
423 issue.reload
435 issue.reload
424 end
436 end
425 end
437 end
General Comments 0
You need to be logged in to leave comments. Login now