##// END OF EJS Templates
Add an "Assigned To" keyword to receiving email. #5594...
Eric Davis -
r3650:e94c45d54841
parent child
Show More
@@ -1,323 +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)
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 assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true)))
123 124 due_date = get_keyword(:due_date, :override => true)
124 125 start_date = get_keyword(:start_date, :override => true)
125 126
126 127 # check permission
127 128 unless @@handler_options[:no_permission_check]
128 129 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
129 130 end
130 131
131 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :due_date => due_date, :start_date => start_date)
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)
132 133 # check workflow
133 134 if status && issue.new_statuses_allowed_to(user).include?(status)
134 135 issue.status = status
135 136 end
136 137 issue.subject = email.subject.chomp
137 138 if issue.subject.blank?
138 139 issue.subject = '(no subject)'
139 140 end
140 141 # custom fields
141 142 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
142 143 if value = get_keyword(c.name, :override => true)
143 144 h[c.id] = value
144 145 end
145 146 h
146 147 end
147 148 issue.description = cleaned_up_text_body
148 149 # add To and Cc as watchers before saving so the watchers can reply to Redmine
149 150 add_watchers(issue)
150 151 issue.save!
151 152 add_attachments(issue)
152 153 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
153 154 issue
154 155 end
155 156
156 157 def target_project
157 158 # TODO: other ways to specify project:
158 159 # * parse the email To field
159 160 # * specific project (eg. Setting.mail_handler_target_project)
160 161 target = Project.find_by_identifier(get_keyword(:project))
161 162 raise MissingInformation.new('Unable to determine target project') if target.nil?
162 163 target
163 164 end
164 165
165 166 # Adds a note to an existing issue
166 167 def receive_issue_reply(issue_id)
167 168 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
168 169 due_date = get_keyword(:due_date, :override => true)
169 170 start_date = get_keyword(:start_date, :override => true)
171 assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true)))
170 172
171 173 issue = Issue.find_by_id(issue_id)
172 174 return unless issue
173 175 # check permission
174 176 unless @@handler_options[:no_permission_check]
175 177 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
176 178 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
177 179 end
178 180
179 181 # add the note
180 182 journal = issue.init_journal(user, cleaned_up_text_body)
181 183 add_attachments(issue)
182 184 # check workflow
183 185 if status && issue.new_statuses_allowed_to(user).include?(status)
184 186 issue.status = status
185 187 end
186 188 issue.start_date = start_date if start_date
187 189 issue.due_date = due_date if due_date
190 issue.assigned_to = assigned_to if assigned_to
188 191
189 192 issue.save!
190 193 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
191 194 journal
192 195 end
193 196
194 197 # Reply will be added to the issue
195 198 def receive_journal_reply(journal_id)
196 199 journal = Journal.find_by_id(journal_id)
197 200 if journal && journal.journalized_type == 'Issue'
198 201 receive_issue_reply(journal.journalized_id)
199 202 end
200 203 end
201 204
202 205 # Receives a reply to a forum message
203 206 def receive_message_reply(message_id)
204 207 message = Message.find_by_id(message_id)
205 208 if message
206 209 message = message.root
207 210
208 211 unless @@handler_options[:no_permission_check]
209 212 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
210 213 end
211 214
212 215 if !message.locked?
213 216 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
214 217 :content => cleaned_up_text_body)
215 218 reply.author = user
216 219 reply.board = message.board
217 220 message.children << reply
218 221 add_attachments(reply)
219 222 reply
220 223 else
221 224 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
222 225 end
223 226 end
224 227 end
225 228
226 229 def add_attachments(obj)
227 230 if email.has_attachments?
228 231 email.attachments.each do |attachment|
229 232 Attachment.create(:container => obj,
230 233 :file => attachment,
231 234 :author => user,
232 235 :content_type => attachment.content_type)
233 236 end
234 237 end
235 238 end
236 239
237 240 # Adds To and Cc as watchers of the given object if the sender has the
238 241 # appropriate permission
239 242 def add_watchers(obj)
240 243 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
241 244 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
242 245 unless addresses.empty?
243 246 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
244 247 watchers.each {|w| obj.add_watcher(w)}
245 248 end
246 249 end
247 250 end
248 251
249 252 def get_keyword(attr, options={})
250 253 @keywords ||= {}
251 254 if @keywords.has_key?(attr)
252 255 @keywords[attr]
253 256 else
254 257 @keywords[attr] = begin
255 258 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr.to_s.humanize}[ \t]*:[ \t]*(.+)\s*$/i, '')
256 259 $1.strip
257 260 elsif !@@handler_options[:issue][attr].blank?
258 261 @@handler_options[:issue][attr]
259 262 end
260 263 end
261 264 end
262 265 end
263 266
264 267 # Returns the text/plain part of the email
265 268 # If not found (eg. HTML-only email), returns the body with tags removed
266 269 def plain_text_body
267 270 return @plain_text_body unless @plain_text_body.nil?
268 271 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
269 272 if parts.empty?
270 273 parts << @email
271 274 end
272 275 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
273 276 if plain_text_part.nil?
274 277 # no text/plain part found, assuming html-only email
275 278 # strip html tags and remove doctype directive
276 279 @plain_text_body = strip_tags(@email.body.to_s)
277 280 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
278 281 else
279 282 @plain_text_body = plain_text_part.body.to_s
280 283 end
281 284 @plain_text_body.strip!
282 285 @plain_text_body
283 286 end
284 287
285 288 def cleaned_up_text_body
286 289 cleanup_body(plain_text_body)
287 290 end
288 291
289 292 def self.full_sanitizer
290 293 @full_sanitizer ||= HTML::FullSanitizer.new
291 294 end
292 295
293 296 # Creates a user account for the +email+ sender
294 297 def self.create_user_from_email(email)
295 298 addr = email.from_addrs.to_a.first
296 299 if addr && !addr.spec.blank?
297 300 user = User.new
298 301 user.mail = addr.spec
299 302
300 303 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
301 304 user.firstname = names.shift
302 305 user.lastname = names.join(' ')
303 306 user.lastname = '-' if user.lastname.blank?
304 307
305 308 user.login = user.mail
306 309 user.password = ActiveSupport::SecureRandom.hex(5)
307 310 user.language = Setting.default_language
308 311 user.save ? user : nil
309 312 end
310 313 end
311 314
312 315 private
313 316
314 317 # Removes the email body of text after the truncation configurations.
315 318 def cleanup_body(body)
316 319 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
317 320 unless delimiters.empty?
318 321 regex = Regexp.new("^(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
319 322 body = body.gsub(regex, '')
320 323 end
321 324 body.strip
322 325 end
326
327 def find_user_from_keyword(keyword)
328 user ||= User.find_by_mail(keyword)
329 user ||= User.find_by_login(keyword)
330 if user.nil? && keyword.match(/ /)
331 firstname, lastname = *(keyword.split) # "First Last Throwaway"
332 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
333 end
334 user
335 end
323 336 end
@@ -1,55 +1,57
1 1 Return-Path: <JSmith@somenet.foo>
2 2 Received: from osiris ([127.0.0.1])
3 3 by OSIRIS
4 4 with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
5 5 Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
6 6 From: "John Smith" <JSmith@somenet.foo>
7 7 To: <redmine@somenet.foo>
8 8 Subject: New ticket on a given project
9 9 Date: Sun, 22 Jun 2008 12:28:07 +0200
10 10 MIME-Version: 1.0
11 11 Content-Type: text/plain;
12 12 format=flowed;
13 13 charset="iso-8859-1";
14 14 reply-type=original
15 15 Content-Transfer-Encoding: 7bit
16 16 X-Priority: 3
17 17 X-MSMail-Priority: Normal
18 18 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
19 19 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
20 20
21 21 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
22 22 turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
23 23 blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
24 24 sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
25 25 in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
26 26 sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
27 27 id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
28 28 eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
29 29 sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et
30 30 malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
31 31 platea dictumst.
32 32
33 33 --- This line starts with a delimiter and should not be stripped
34 34
35 35 This paragraph is before delimiters.
36 36
37 37 BREAK
38 38
39 39 This paragraph is between delimiters.
40 40
41 41 ---
42 42
43 43 This paragraph is after the delimiter so it shouldn't appear.
44 44
45 45 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
46 46 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
47 47 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
48 48 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
49 49 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
50 50 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
51 51
52 52 Project: onlinestore
53 53 Status: Resolved
54 54 due date: 2010-12-31
55 55 Start Date:2010-01-01
56 Assigned to: John Smith
57
@@ -1,78 +1,79
1 1 Return-Path: <jsmith@somenet.foo>
2 2 Received: from osiris ([127.0.0.1])
3 3 by OSIRIS
4 4 with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200
5 5 Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris>
6 6 From: "John Smith" <jsmith@somenet.foo>
7 7 To: <redmine@somenet.foo>
8 8 References: <485d0ad366c88_d7014663a025f@osiris.tmail>
9 9 Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories
10 10 Date: Sat, 21 Jun 2008 18:41:39 +0200
11 11 MIME-Version: 1.0
12 12 Content-Type: multipart/alternative;
13 13 boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"
14 14 X-Priority: 3
15 15 X-MSMail-Priority: Normal
16 16 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
17 17 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
18 18
19 19 This is a multi-part message in MIME format.
20 20
21 21 ------=_NextPart_000_0067_01C8D3CE.711F9CC0
22 22 Content-Type: text/plain;
23 23 charset="utf-8"
24 24 Content-Transfer-Encoding: quoted-printable
25 25
26 26 This is reply
27 27
28 28 Status: Resolved
29 29 due date: 2010-12-31
30 30 Start Date:2010-01-01
31 Assigned to: jsmith@somenet.foo
31 32
32 33 ------=_NextPart_000_0067_01C8D3CE.711F9CC0
33 34 Content-Type: text/html;
34 35 charset="utf-8"
35 36 Content-Transfer-Encoding: quoted-printable
36 37
37 38 =EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
38 39 <HTML><HEAD>
39 40 <META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">
40 41 <STYLE>BODY {
41 42 FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif
42 43 }
43 44 BODY H1 {
44 45 FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =
45 46 sans-serif
46 47 }
47 48 A {
48 49 COLOR: #2a5685
49 50 }
50 51 A:link {
51 52 COLOR: #2a5685
52 53 }
53 54 A:visited {
54 55 COLOR: #2a5685
55 56 }
56 57 A:hover {
57 58 COLOR: #c61a1a
58 59 }
59 60 A:active {
60 61 COLOR: #c61a1a
61 62 }
62 63 HR {
63 64 BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =
64 65 WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px
65 66 }
66 67 .footer {
67 68 FONT-SIZE: 0.8em; FONT-STYLE: italic
68 69 }
69 70 </STYLE>
70 71
71 72 <META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>
72 73 <BODY bgColor=3D#ffffff>
73 74 <DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =
74 75 size=3D2>This is=20
75 76 reply Status: Resolved</FONT></DIV></SPAN></BODY></HTML>
76 77
77 78 ------=_NextPart_000_0067_01C8D3CE.711F9CC0--
78 79
@@ -1,354 +1,356
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 end
45 45
46 46 def test_add_issue
47 47 ActionMailer::Base.deliveries.clear
48 48 # This email contains: 'Project: onlinestore'
49 49 issue = submit_email('ticket_on_given_project.eml')
50 50 assert issue.is_a?(Issue)
51 51 assert !issue.new_record?
52 52 issue.reload
53 53 assert_equal 'New ticket on a given project', issue.subject
54 54 assert_equal User.find_by_login('jsmith'), issue.author
55 55 assert_equal Project.find(2), issue.project
56 56 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
57 57 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
58 58 assert_equal '2010-01-01', issue.start_date.to_s
59 59 assert_equal '2010-12-31', issue.due_date.to_s
60 assert_equal User.find_by_login('jsmith'), issue.assigned_to
60 61 # keywords should be removed from the email body
61 62 assert !issue.description.match(/^Project:/i)
62 63 assert !issue.description.match(/^Status:/i)
63 64 # Email notification should be sent
64 65 mail = ActionMailer::Base.deliveries.last
65 66 assert_not_nil mail
66 67 assert mail.subject.include?('New ticket on a given project')
67 68 end
68 69
69 70 def test_add_issue_with_status
70 71 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
71 72 issue = submit_email('ticket_on_given_project.eml')
72 73 assert issue.is_a?(Issue)
73 74 assert !issue.new_record?
74 75 issue.reload
75 76 assert_equal Project.find(2), issue.project
76 77 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
77 78 end
78 79
79 80 def test_add_issue_with_attributes_override
80 81 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
81 82 assert issue.is_a?(Issue)
82 83 assert !issue.new_record?
83 84 issue.reload
84 85 assert_equal 'New ticket on a given project', issue.subject
85 86 assert_equal User.find_by_login('jsmith'), issue.author
86 87 assert_equal Project.find(2), issue.project
87 88 assert_equal 'Feature request', issue.tracker.to_s
88 89 assert_equal 'Stock management', issue.category.to_s
89 90 assert_equal 'Urgent', issue.priority.to_s
90 91 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
91 92 end
92 93
93 94 def test_add_issue_with_partial_attributes_override
94 95 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
95 96 assert issue.is_a?(Issue)
96 97 assert !issue.new_record?
97 98 issue.reload
98 99 assert_equal 'New ticket on a given project', issue.subject
99 100 assert_equal User.find_by_login('jsmith'), issue.author
100 101 assert_equal Project.find(2), issue.project
101 102 assert_equal 'Feature request', issue.tracker.to_s
102 103 assert_nil issue.category
103 104 assert_equal 'High', issue.priority.to_s
104 105 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
105 106 end
106 107
107 108 def test_add_issue_with_spaces_between_attribute_and_separator
108 109 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
109 110 assert issue.is_a?(Issue)
110 111 assert !issue.new_record?
111 112 issue.reload
112 113 assert_equal 'New ticket on a given project', issue.subject
113 114 assert_equal User.find_by_login('jsmith'), issue.author
114 115 assert_equal Project.find(2), issue.project
115 116 assert_equal 'Feature request', issue.tracker.to_s
116 117 assert_equal 'Stock management', issue.category.to_s
117 118 assert_equal 'Urgent', issue.priority.to_s
118 119 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
119 120 end
120 121
121 122
122 123 def test_add_issue_with_attachment_to_specific_project
123 124 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
124 125 assert issue.is_a?(Issue)
125 126 assert !issue.new_record?
126 127 issue.reload
127 128 assert_equal 'Ticket created by email with attachment', issue.subject
128 129 assert_equal User.find_by_login('jsmith'), issue.author
129 130 assert_equal Project.find(2), issue.project
130 131 assert_equal 'This is a new ticket with attachments', issue.description
131 132 # Attachment properties
132 133 assert_equal 1, issue.attachments.size
133 134 assert_equal 'Paella.jpg', issue.attachments.first.filename
134 135 assert_equal 'image/jpeg', issue.attachments.first.content_type
135 136 assert_equal 10790, issue.attachments.first.filesize
136 137 end
137 138
138 139 def test_add_issue_with_custom_fields
139 140 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
140 141 assert issue.is_a?(Issue)
141 142 assert !issue.new_record?
142 143 issue.reload
143 144 assert_equal 'New ticket with custom field values', issue.subject
144 145 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
145 146 assert !issue.description.match(/^searchable field:/i)
146 147 end
147 148
148 149 def test_add_issue_with_cc
149 150 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
150 151 assert issue.is_a?(Issue)
151 152 assert !issue.new_record?
152 153 issue.reload
153 154 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
154 155 assert_equal 1, issue.watchers.size
155 156 end
156 157
157 158 def test_add_issue_by_unknown_user
158 159 assert_no_difference 'User.count' do
159 160 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
160 161 end
161 162 end
162 163
163 164 def test_add_issue_by_anonymous_user
164 165 Role.anonymous.add_permission!(:add_issues)
165 166 assert_no_difference 'User.count' do
166 167 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
167 168 assert issue.is_a?(Issue)
168 169 assert issue.author.anonymous?
169 170 end
170 171 end
171 172
172 173 def test_add_issue_by_anonymous_user_on_private_project
173 174 Role.anonymous.add_permission!(:add_issues)
174 175 assert_no_difference 'User.count' do
175 176 assert_no_difference 'Issue.count' do
176 177 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
177 178 end
178 179 end
179 180 end
180 181
181 182 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
182 183 assert_no_difference 'User.count' do
183 184 assert_difference 'Issue.count' do
184 185 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
185 186 assert issue.is_a?(Issue)
186 187 assert issue.author.anonymous?
187 188 assert !issue.project.is_public?
188 189 end
189 190 end
190 191 end
191 192
192 193 def test_add_issue_by_created_user
193 194 Setting.default_language = 'en'
194 195 assert_difference 'User.count' do
195 196 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
196 197 assert issue.is_a?(Issue)
197 198 assert issue.author.active?
198 199 assert_equal 'john.doe@somenet.foo', issue.author.mail
199 200 assert_equal 'John', issue.author.firstname
200 201 assert_equal 'Doe', issue.author.lastname
201 202
202 203 # account information
203 204 email = ActionMailer::Base.deliveries.first
204 205 assert_not_nil email
205 206 assert email.subject.include?('account activation')
206 207 login = email.body.match(/\* Login: (.*)$/)[1]
207 208 password = email.body.match(/\* Password: (.*)$/)[1]
208 209 assert_equal issue.author, User.try_to_login(login, password)
209 210 end
210 211 end
211 212
212 213 def test_add_issue_without_from_header
213 214 Role.anonymous.add_permission!(:add_issues)
214 215 assert_equal false, submit_email('ticket_without_from_header.eml')
215 216 end
216 217
217 218 def test_add_issue_with_japanese_keywords
218 219 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
219 220 Project.find(1).trackers << tracker
220 221 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
221 222 assert_kind_of Issue, issue
222 223 assert_equal tracker, issue.tracker
223 224 end
224 225
225 226 def test_should_ignore_emails_from_emission_address
226 227 Role.anonymous.add_permission!(:add_issues)
227 228 assert_no_difference 'User.count' do
228 229 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
229 230 end
230 231 end
231 232
232 233 def test_add_issue_should_send_email_notification
233 234 ActionMailer::Base.deliveries.clear
234 235 # This email contains: 'Project: onlinestore'
235 236 issue = submit_email('ticket_on_given_project.eml')
236 237 assert issue.is_a?(Issue)
237 238 assert_equal 1, ActionMailer::Base.deliveries.size
238 239 end
239 240
240 241 def test_add_issue_note
241 242 journal = submit_email('ticket_reply.eml')
242 243 assert journal.is_a?(Journal)
243 244 assert_equal User.find_by_login('jsmith'), journal.user
244 245 assert_equal Issue.find(2), journal.journalized
245 246 assert_match /This is reply/, journal.notes
246 247 end
247 248
248 249 def test_add_issue_note_with_attribute_changes
249 250 # This email contains: 'Status: Resolved'
250 251 journal = submit_email('ticket_reply_with_status.eml')
251 252 assert journal.is_a?(Journal)
252 253 issue = Issue.find(journal.issue.id)
253 254 assert_equal User.find_by_login('jsmith'), journal.user
254 255 assert_equal Issue.find(2), journal.journalized
255 256 assert_match /This is reply/, journal.notes
256 257 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
257 258 assert_equal '2010-01-01', issue.start_date.to_s
258 259 assert_equal '2010-12-31', issue.due_date.to_s
260 assert_equal User.find_by_login('jsmith'), issue.assigned_to
259 261 end
260 262
261 263 def test_add_issue_note_should_send_email_notification
262 264 ActionMailer::Base.deliveries.clear
263 265 journal = submit_email('ticket_reply.eml')
264 266 assert journal.is_a?(Journal)
265 267 assert_equal 1, ActionMailer::Base.deliveries.size
266 268 end
267 269
268 270 def test_reply_to_a_message
269 271 m = submit_email('message_reply.eml')
270 272 assert m.is_a?(Message)
271 273 assert !m.new_record?
272 274 m.reload
273 275 assert_equal 'Reply via email', m.subject
274 276 # The email replies to message #2 which is part of the thread of message #1
275 277 assert_equal Message.find(1), m.parent
276 278 end
277 279
278 280 def test_reply_to_a_message_by_subject
279 281 m = submit_email('message_reply_by_subject.eml')
280 282 assert m.is_a?(Message)
281 283 assert !m.new_record?
282 284 m.reload
283 285 assert_equal 'Reply to the first post', m.subject
284 286 assert_equal Message.find(1), m.parent
285 287 end
286 288
287 289 def test_should_strip_tags_of_html_only_emails
288 290 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
289 291 assert issue.is_a?(Issue)
290 292 assert !issue.new_record?
291 293 issue.reload
292 294 assert_equal 'HTML email', issue.subject
293 295 assert_equal 'This is a html-only email.', issue.description
294 296 end
295 297
296 298 context "truncate emails based on the Setting" do
297 299 context "with no setting" do
298 300 setup do
299 301 Setting.mail_handler_body_delimiters = ''
300 302 end
301 303
302 304 should "add the entire email into the issue" do
303 305 issue = submit_email('ticket_on_given_project.eml')
304 306 assert_issue_created(issue)
305 307 assert issue.description.include?('---')
306 308 assert issue.description.include?('This paragraph is after the delimiter')
307 309 end
308 310 end
309 311
310 312 context "with a single string" do
311 313 setup do
312 314 Setting.mail_handler_body_delimiters = '---'
313 315 end
314 316
315 317 should "truncate the email at the delimiter for the issue" do
316 318 issue = submit_email('ticket_on_given_project.eml')
317 319 assert_issue_created(issue)
318 320 assert issue.description.include?('This paragraph is before delimiters')
319 321 assert issue.description.include?('--- This line starts with a delimiter')
320 322 assert !issue.description.match(/^---$/)
321 323 assert !issue.description.include?('This paragraph is after the delimiter')
322 324 end
323 325 end
324 326
325 327 context "with multiple strings" do
326 328 setup do
327 329 Setting.mail_handler_body_delimiters = "---\nBREAK"
328 330 end
329 331
330 332 should "truncate the email at the first delimiter found (BREAK)" do
331 333 issue = submit_email('ticket_on_given_project.eml')
332 334 assert_issue_created(issue)
333 335 assert issue.description.include?('This paragraph is before delimiters')
334 336 assert !issue.description.include?('BREAK')
335 337 assert !issue.description.include?('This paragraph is between delimiters')
336 338 assert !issue.description.match(/^---$/)
337 339 assert !issue.description.include?('This paragraph is after the delimiter')
338 340 end
339 341 end
340 342 end
341 343
342 344 private
343 345
344 346 def submit_email(filename, options={})
345 347 raw = IO.read(File.join(FIXTURES_PATH, filename))
346 348 MailHandler.receive(raw, options)
347 349 end
348 350
349 351 def assert_issue_created(issue)
350 352 assert issue.is_a?(Issue)
351 353 assert !issue.new_record?
352 354 issue.reload
353 355 end
354 356 end
General Comments 0
You need to be logged in to leave comments. Login now