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