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