##// END OF EJS Templates
fix non ASCII attachment filename encoding broken (MOJIBAKE) in receiving mail on Ruby 1.8 (#12399)...
Toshi MARUYAMA -
r10625:6740f441c499
parent child
Show More
@@ -0,0 +1,26
1 Date: Tue, 20 Nov 2012 23:08:25 +0900
2 Message-ID: <CANBr5-UZM=Odz4U3Q6vHd_9cd2tCT-_P9xDd=hRJ0aoMNTWXbw@mail.gmail.com>
3 Subject: test
4 From: John Smith <JSmith@somenet.foo>
5 To: redmine@somenet.foo
6 Content-Type: multipart/mixed; boundary=14dae93a13bf76ca5d04ceedc458
7
8 --14dae93a13bf76ca5d04ceedc458
9 Content-Type: text/plain; charset=ISO-8859-1
10
11 test
12
13 --14dae93a13bf76ca5d04ceedc458
14 Content-Type: text/plain; charset=US-ASCII;
15 name="=?ISO-8859-1?B?xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tw=?=
16 =?ISO-8859-1?B?/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc?=
17 =?ISO-8859-1?B?/MTk1vbc/C50eHQ=?="
18 Content-Disposition: attachment;
19 filename="=?ISO-8859-1?B?xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tw=?=
20 =?ISO-8859-1?B?/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc?=
21 =?ISO-8859-1?B?/MTk1vbc/C50eHQ=?="
22 Content-Transfer-Encoding: base64
23 X-Attachment-Id: f_h9r3mcjz0
24
25 dGVzdAo=
26 --14dae93a13bf76ca5d04ceedc458--
@@ -0,0 +1,20
1 Date: Mon, 19 Nov 2012 10:17:45 +0900
2 Message-ID: <CANBr5-U6cXMfLek5QiB2ZrBPR3vTThn9_Upvdkf3Dkod664+Xw@mail.gmail.com>
3 Subject: test
4 From: John Smith <JSmith@somenet.foo>
5 To: redmine@somenet.foo
6 Content-Type: multipart/mixed; boundary=bcaec54ee4ea84f77904cecee22e
7
8 --bcaec54ee4ea84f77904cecee22e
9 Content-Type: text/plain; charset=ISO-8859-1
10
11 test
12
13 --bcaec54ee4ea84f77904cecee22e
14 Content-Type: text/plain; charset=US-ASCII; name="=?ISO-2022-JP?B?GyRCJUYlOSVIGyhCLnR4dA==?="
15 Content-Disposition: attachment; filename="=?ISO-2022-JP?B?GyRCJUYlOSVIGyhCLnR4dA==?="
16 Content-Transfer-Encoding: base64
17 X-Attachment-Id: f_h9owndpv0
18
19 dGVzdAo=
20 --bcaec54ee4ea84f77904cecee22e--
@@ -0,0 +1,34
1 Message-ID: <50AB9546.7020800@gmail.com>
2 Date: Tue, 20 Nov 2012 23:35:50 +0900
3 From: John Smith <JSmith@somenet.foo>
4 User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.17) Gecko/20110428 Fedora/3.1.10-1.fc13 Thunderbird/3.1.10
5 MIME-Version: 1.0
6 To: redmine@somenet.foo
7 Subject: test
8 Content-Type: multipart/mixed;
9 boundary="------------050902080306030406090208"
10
11 This is a multi-part message in MIME format.
12 --------------050902080306030406090208
13 Content-Type: text/plain; charset=ISO-8859-1; format=flowed
14 Content-Transfer-Encoding: 7bit
15
16 test
17
18 --------------050902080306030406090208
19 Content-Type: image/png;
20 name="=?ISO-8859-1?Q?=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4?=
21 =?ISO-8859-1?Q?=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6?=
22 =?ISO-8859-1?Q?=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6=DC=FC=C4=E4=D6=F6?=
23 =?ISO-8859-1?Q?=DC=FC=C4=E4=D6=F6=DC=FC=2Epng?="
24 Content-Transfer-Encoding: base64
25 Content-Disposition: attachment;
26 filename*0*=ISO-8859-1''%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6;
27 filename*1*=%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC;
28 filename*2*=%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%C4%E4;
29 filename*3*=%D6%F6%DC%FC%C4%E4%D6%F6%DC%FC%2E%70%6E%67
30
31 iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAAXNSR0IArs4c6QAAAAlwSFlz
32 AAALEwAACxMBAJqcGAAAAAd0SU1FB9wLFA4fJhRKIUQAAAAUSURBVAjXY/z//z8DEmBiQAWk
33 8gHq9gMHP8uZWAAAAABJRU5ErkJggg==
34 --------------050902080306030406090208--
@@ -0,0 +1,26
1 Message-ID: <50AA00C6.4070108@gmail.com>
2 Date: Mon, 19 Nov 2012 18:49:58 +0900
3 From: John Smith <JSmith@somenet.foo>
4 User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.15) Gecko/20101027 Fedora/3.0.10-1.fc12 Lightning/1.0b1 Thunderbird/3.0.10
5 MIME-Version: 1.0
6 To: redmine@somenet.foo
7 Subject: test
8 Content-Type: multipart/mixed;
9 boundary="------------030104060902010800050907"
10
11 This is a multi-part message in MIME format.
12 --------------030104060902010800050907
13 Content-Type: text/plain; charset=ISO-2022-JP
14 Content-Transfer-Encoding: 7bit
15
16 test
17
18 --------------030104060902010800050907
19 Content-Type: text/plain;
20 name="=?ISO-2022-JP?B?GyRCJUYlOSVIGyhCLnR4dA==?="
21 Content-Transfer-Encoding: base64
22 Content-Disposition: attachment;
23 filename*=ISO-2022-JP''%1B%24%42%25%46%25%39%25%48%1B%28%42%2E%74%78%74
24
25 dGVzdAo=
26 --------------030104060902010800050907--
@@ -1,481 +1,498
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user
25 attr_reader :email, :user
26
26
27 def self.receive(email, options={})
27 def self.receive(email, options={})
28 @@handler_options = options.dup
28 @@handler_options = options.dup
29
29
30 @@handler_options[:issue] ||= {}
30 @@handler_options[:issue] ||= {}
31
31
32 if @@handler_options[:allow_override].is_a?(String)
32 if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 end
34 end
35 @@handler_options[:allow_override] ||= []
35 @@handler_options[:allow_override] ||= []
36 # Project needs to be overridable if not specified
36 # Project needs to be overridable if not specified
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 # Status overridable by default
38 # Status overridable by default
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40
40
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
42
42
43 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
43 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
44 super(email)
44 super(email)
45 end
45 end
46
46
47 def logger
47 def logger
48 Rails.logger
48 Rails.logger
49 end
49 end
50
50
51 cattr_accessor :ignored_emails_headers
51 cattr_accessor :ignored_emails_headers
52 @@ignored_emails_headers = {
52 @@ignored_emails_headers = {
53 'X-Auto-Response-Suppress' => 'oof',
53 'X-Auto-Response-Suppress' => 'oof',
54 'Auto-Submitted' => /^auto-/
54 'Auto-Submitted' => /^auto-/
55 }
55 }
56
56
57 # Processes incoming emails
57 # Processes incoming emails
58 # Returns the created object (eg. an issue, a message) or false
58 # Returns the created object (eg. an issue, a message) or false
59 def receive(email)
59 def receive(email)
60 @email = email
60 @email = email
61 sender_email = email.from.to_a.first.to_s.strip
61 sender_email = email.from.to_a.first.to_s.strip
62 # Ignore emails received from the application emission address to avoid hell cycles
62 # Ignore emails received from the application emission address to avoid hell cycles
63 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
63 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
64 if logger && logger.info
64 if logger && logger.info
65 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
65 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
66 end
66 end
67 return false
67 return false
68 end
68 end
69 # Ignore auto generated emails
69 # Ignore auto generated emails
70 self.class.ignored_emails_headers.each do |key, ignored_value|
70 self.class.ignored_emails_headers.each do |key, ignored_value|
71 value = email.header[key]
71 value = email.header[key]
72 if value
72 if value
73 value = value.to_s.downcase
73 value = value.to_s.downcase
74 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
74 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
75 if logger && logger.info
75 if logger && logger.info
76 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
76 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
77 end
77 end
78 return false
78 return false
79 end
79 end
80 end
80 end
81 end
81 end
82 @user = User.find_by_mail(sender_email) if sender_email.present?
82 @user = User.find_by_mail(sender_email) if sender_email.present?
83 if @user && !@user.active?
83 if @user && !@user.active?
84 if logger && logger.info
84 if logger && logger.info
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
86 end
86 end
87 return false
87 return false
88 end
88 end
89 if @user.nil?
89 if @user.nil?
90 # Email was submitted by an unknown user
90 # Email was submitted by an unknown user
91 case @@handler_options[:unknown_user]
91 case @@handler_options[:unknown_user]
92 when 'accept'
92 when 'accept'
93 @user = User.anonymous
93 @user = User.anonymous
94 when 'create'
94 when 'create'
95 @user = create_user_from_email
95 @user = create_user_from_email
96 if @user
96 if @user
97 if logger && logger.info
97 if logger && logger.info
98 logger.info "MailHandler: [#{@user.login}] account created"
98 logger.info "MailHandler: [#{@user.login}] account created"
99 end
99 end
100 Mailer.account_information(@user, @user.password).deliver
100 Mailer.account_information(@user, @user.password).deliver
101 else
101 else
102 if logger && logger.error
102 if logger && logger.error
103 logger.error "MailHandler: could not create account for [#{sender_email}]"
103 logger.error "MailHandler: could not create account for [#{sender_email}]"
104 end
104 end
105 return false
105 return false
106 end
106 end
107 else
107 else
108 # Default behaviour, emails from unknown users are ignored
108 # Default behaviour, emails from unknown users are ignored
109 if logger && logger.info
109 if logger && logger.info
110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
111 end
111 end
112 return false
112 return false
113 end
113 end
114 end
114 end
115 User.current = @user
115 User.current = @user
116 dispatch
116 dispatch
117 end
117 end
118
118
119 private
119 private
120
120
121 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
121 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
124
124
125 def dispatch
125 def dispatch
126 headers = [email.in_reply_to, email.references].flatten.compact
126 headers = [email.in_reply_to, email.references].flatten.compact
127 subject = email.subject.to_s
127 subject = email.subject.to_s
128 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
128 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
129 klass, object_id = $1, $2.to_i
129 klass, object_id = $1, $2.to_i
130 method_name = "receive_#{klass}_reply"
130 method_name = "receive_#{klass}_reply"
131 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
131 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
132 send method_name, object_id
132 send method_name, object_id
133 else
133 else
134 # ignoring it
134 # ignoring it
135 end
135 end
136 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
136 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
137 receive_issue_reply(m[1].to_i)
137 receive_issue_reply(m[1].to_i)
138 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
138 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
139 receive_message_reply(m[1].to_i)
139 receive_message_reply(m[1].to_i)
140 else
140 else
141 dispatch_to_default
141 dispatch_to_default
142 end
142 end
143 rescue ActiveRecord::RecordInvalid => e
143 rescue ActiveRecord::RecordInvalid => e
144 # TODO: send a email to the user
144 # TODO: send a email to the user
145 logger.error e.message if logger
145 logger.error e.message if logger
146 false
146 false
147 rescue MissingInformation => e
147 rescue MissingInformation => e
148 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
148 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
149 false
149 false
150 rescue UnauthorizedAction => e
150 rescue UnauthorizedAction => e
151 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
151 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
152 false
152 false
153 end
153 end
154
154
155 def dispatch_to_default
155 def dispatch_to_default
156 receive_issue
156 receive_issue
157 end
157 end
158
158
159 # Creates a new issue
159 # Creates a new issue
160 def receive_issue
160 def receive_issue
161 project = target_project
161 project = target_project
162 # check permission
162 # check permission
163 unless @@handler_options[:no_permission_check]
163 unless @@handler_options[:no_permission_check]
164 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
164 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
165 end
165 end
166
166
167 issue = Issue.new(:author => user, :project => project)
167 issue = Issue.new(:author => user, :project => project)
168 issue.safe_attributes = issue_attributes_from_keywords(issue)
168 issue.safe_attributes = issue_attributes_from_keywords(issue)
169 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
169 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
170 issue.subject = cleaned_up_subject
170 issue.subject = cleaned_up_subject
171 if issue.subject.blank?
171 if issue.subject.blank?
172 issue.subject = '(no subject)'
172 issue.subject = '(no subject)'
173 end
173 end
174 issue.description = cleaned_up_text_body
174 issue.description = cleaned_up_text_body
175
175
176 # add To and Cc as watchers before saving so the watchers can reply to Redmine
176 # add To and Cc as watchers before saving so the watchers can reply to Redmine
177 add_watchers(issue)
177 add_watchers(issue)
178 issue.save!
178 issue.save!
179 add_attachments(issue)
179 add_attachments(issue)
180 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
180 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
181 issue
181 issue
182 end
182 end
183
183
184 # Adds a note to an existing issue
184 # Adds a note to an existing issue
185 def receive_issue_reply(issue_id, from_journal=nil)
185 def receive_issue_reply(issue_id, from_journal=nil)
186 issue = Issue.find_by_id(issue_id)
186 issue = Issue.find_by_id(issue_id)
187 return unless issue
187 return unless issue
188 # check permission
188 # check permission
189 unless @@handler_options[:no_permission_check]
189 unless @@handler_options[:no_permission_check]
190 unless user.allowed_to?(:add_issue_notes, issue.project) ||
190 unless user.allowed_to?(:add_issue_notes, issue.project) ||
191 user.allowed_to?(:edit_issues, issue.project)
191 user.allowed_to?(:edit_issues, issue.project)
192 raise UnauthorizedAction
192 raise UnauthorizedAction
193 end
193 end
194 end
194 end
195
195
196 # ignore CLI-supplied defaults for new issues
196 # ignore CLI-supplied defaults for new issues
197 @@handler_options[:issue].clear
197 @@handler_options[:issue].clear
198
198
199 journal = issue.init_journal(user)
199 journal = issue.init_journal(user)
200 if from_journal && from_journal.private_notes?
200 if from_journal && from_journal.private_notes?
201 # If the received email was a reply to a private note, make the added note private
201 # If the received email was a reply to a private note, make the added note private
202 issue.private_notes = true
202 issue.private_notes = true
203 end
203 end
204 issue.safe_attributes = issue_attributes_from_keywords(issue)
204 issue.safe_attributes = issue_attributes_from_keywords(issue)
205 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
205 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
206 journal.notes = cleaned_up_text_body
206 journal.notes = cleaned_up_text_body
207 add_attachments(issue)
207 add_attachments(issue)
208 issue.save!
208 issue.save!
209 if logger && logger.info
209 if logger && logger.info
210 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
210 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
211 end
211 end
212 journal
212 journal
213 end
213 end
214
214
215 # Reply will be added to the issue
215 # Reply will be added to the issue
216 def receive_journal_reply(journal_id)
216 def receive_journal_reply(journal_id)
217 journal = Journal.find_by_id(journal_id)
217 journal = Journal.find_by_id(journal_id)
218 if journal && journal.journalized_type == 'Issue'
218 if journal && journal.journalized_type == 'Issue'
219 receive_issue_reply(journal.journalized_id, journal)
219 receive_issue_reply(journal.journalized_id, journal)
220 end
220 end
221 end
221 end
222
222
223 # Receives a reply to a forum message
223 # Receives a reply to a forum message
224 def receive_message_reply(message_id)
224 def receive_message_reply(message_id)
225 message = Message.find_by_id(message_id)
225 message = Message.find_by_id(message_id)
226 if message
226 if message
227 message = message.root
227 message = message.root
228
228
229 unless @@handler_options[:no_permission_check]
229 unless @@handler_options[:no_permission_check]
230 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
230 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
231 end
231 end
232
232
233 if !message.locked?
233 if !message.locked?
234 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
234 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
235 :content => cleaned_up_text_body)
235 :content => cleaned_up_text_body)
236 reply.author = user
236 reply.author = user
237 reply.board = message.board
237 reply.board = message.board
238 message.children << reply
238 message.children << reply
239 add_attachments(reply)
239 add_attachments(reply)
240 reply
240 reply
241 else
241 else
242 if logger && logger.info
242 if logger && logger.info
243 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
243 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
244 end
244 end
245 end
245 end
246 end
246 end
247 end
247 end
248
248
249 def add_attachments(obj)
249 def add_attachments(obj)
250 if email.attachments && email.attachments.any?
250 if email.attachments && email.attachments.any?
251 email.attachments.each do |attachment|
251 email.attachments.each do |attachment|
252 filename = attachment.filename
253 unless filename.respond_to?(:encoding)
254 # try to reencode to utf8 manually with ruby1.8
255 h = attachment.header['Content-Disposition']
256 unless h.nil?
257 begin
258 if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
259 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
260 elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
261 # http://tools.ietf.org/html/rfc2047#section-4
262 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
263 end
264 rescue
265 # nop
266 end
267 end
268 end
252 obj.attachments << Attachment.create(:container => obj,
269 obj.attachments << Attachment.create(:container => obj,
253 :file => attachment.decoded,
270 :file => attachment.decoded,
254 :filename => attachment.filename,
271 :filename => filename,
255 :author => user,
272 :author => user,
256 :content_type => attachment.mime_type)
273 :content_type => attachment.mime_type)
257 end
274 end
258 end
275 end
259 end
276 end
260
277
261 # Adds To and Cc as watchers of the given object if the sender has the
278 # Adds To and Cc as watchers of the given object if the sender has the
262 # appropriate permission
279 # appropriate permission
263 def add_watchers(obj)
280 def add_watchers(obj)
264 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
281 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
265 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
282 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
266 unless addresses.empty?
283 unless addresses.empty?
267 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
284 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
268 watchers.each {|w| obj.add_watcher(w)}
285 watchers.each {|w| obj.add_watcher(w)}
269 end
286 end
270 end
287 end
271 end
288 end
272
289
273 def get_keyword(attr, options={})
290 def get_keyword(attr, options={})
274 @keywords ||= {}
291 @keywords ||= {}
275 if @keywords.has_key?(attr)
292 if @keywords.has_key?(attr)
276 @keywords[attr]
293 @keywords[attr]
277 else
294 else
278 @keywords[attr] = begin
295 @keywords[attr] = begin
279 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
296 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
280 (v = extract_keyword!(plain_text_body, attr, options[:format]))
297 (v = extract_keyword!(plain_text_body, attr, options[:format]))
281 v
298 v
282 elsif !@@handler_options[:issue][attr].blank?
299 elsif !@@handler_options[:issue][attr].blank?
283 @@handler_options[:issue][attr]
300 @@handler_options[:issue][attr]
284 end
301 end
285 end
302 end
286 end
303 end
287 end
304 end
288
305
289 # Destructively extracts the value for +attr+ in +text+
306 # Destructively extracts the value for +attr+ in +text+
290 # Returns nil if no matching keyword found
307 # Returns nil if no matching keyword found
291 def extract_keyword!(text, attr, format=nil)
308 def extract_keyword!(text, attr, format=nil)
292 keys = [attr.to_s.humanize]
309 keys = [attr.to_s.humanize]
293 if attr.is_a?(Symbol)
310 if attr.is_a?(Symbol)
294 if user && user.language.present?
311 if user && user.language.present?
295 keys << l("field_#{attr}", :default => '', :locale => user.language)
312 keys << l("field_#{attr}", :default => '', :locale => user.language)
296 end
313 end
297 if Setting.default_language.present?
314 if Setting.default_language.present?
298 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
315 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
299 end
316 end
300 end
317 end
301 keys.reject! {|k| k.blank?}
318 keys.reject! {|k| k.blank?}
302 keys.collect! {|k| Regexp.escape(k)}
319 keys.collect! {|k| Regexp.escape(k)}
303 format ||= '.+'
320 format ||= '.+'
304 keyword = nil
321 keyword = nil
305 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
322 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
306 if m = text.match(regexp)
323 if m = text.match(regexp)
307 keyword = m[2].strip
324 keyword = m[2].strip
308 text.gsub!(regexp, '')
325 text.gsub!(regexp, '')
309 end
326 end
310 keyword
327 keyword
311 end
328 end
312
329
313 def target_project
330 def target_project
314 # TODO: other ways to specify project:
331 # TODO: other ways to specify project:
315 # * parse the email To field
332 # * parse the email To field
316 # * specific project (eg. Setting.mail_handler_target_project)
333 # * specific project (eg. Setting.mail_handler_target_project)
317 target = Project.find_by_identifier(get_keyword(:project))
334 target = Project.find_by_identifier(get_keyword(:project))
318 raise MissingInformation.new('Unable to determine target project') if target.nil?
335 raise MissingInformation.new('Unable to determine target project') if target.nil?
319 target
336 target
320 end
337 end
321
338
322 # Returns a Hash of issue attributes extracted from keywords in the email body
339 # Returns a Hash of issue attributes extracted from keywords in the email body
323 def issue_attributes_from_keywords(issue)
340 def issue_attributes_from_keywords(issue)
324 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
341 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
325
342
326 attrs = {
343 attrs = {
327 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
344 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
328 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
345 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
329 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
346 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
330 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
347 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
331 'assigned_to_id' => assigned_to.try(:id),
348 'assigned_to_id' => assigned_to.try(:id),
332 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
349 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
333 issue.project.shared_versions.named(k).first.try(:id),
350 issue.project.shared_versions.named(k).first.try(:id),
334 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
351 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
335 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
352 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
336 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
353 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
337 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
354 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
338 }.delete_if {|k, v| v.blank? }
355 }.delete_if {|k, v| v.blank? }
339
356
340 if issue.new_record? && attrs['tracker_id'].nil?
357 if issue.new_record? && attrs['tracker_id'].nil?
341 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
358 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
342 end
359 end
343
360
344 attrs
361 attrs
345 end
362 end
346
363
347 # Returns a Hash of issue custom field values extracted from keywords in the email body
364 # Returns a Hash of issue custom field values extracted from keywords in the email body
348 def custom_field_values_from_keywords(customized)
365 def custom_field_values_from_keywords(customized)
349 customized.custom_field_values.inject({}) do |h, v|
366 customized.custom_field_values.inject({}) do |h, v|
350 if keyword = get_keyword(v.custom_field.name, :override => true)
367 if keyword = get_keyword(v.custom_field.name, :override => true)
351 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
368 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
352 end
369 end
353 h
370 h
354 end
371 end
355 end
372 end
356
373
357 # Returns the text/plain part of the email
374 # Returns the text/plain part of the email
358 # If not found (eg. HTML-only email), returns the body with tags removed
375 # If not found (eg. HTML-only email), returns the body with tags removed
359 def plain_text_body
376 def plain_text_body
360 return @plain_text_body unless @plain_text_body.nil?
377 return @plain_text_body unless @plain_text_body.nil?
361
378
362 part = email.text_part || email.html_part || email
379 part = email.text_part || email.html_part || email
363 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
380 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
364
381
365 # strip html tags and remove doctype directive
382 # strip html tags and remove doctype directive
366 @plain_text_body = strip_tags(@plain_text_body.strip)
383 @plain_text_body = strip_tags(@plain_text_body.strip)
367 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
384 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
368 @plain_text_body
385 @plain_text_body
369 end
386 end
370
387
371 def cleaned_up_text_body
388 def cleaned_up_text_body
372 cleanup_body(plain_text_body)
389 cleanup_body(plain_text_body)
373 end
390 end
374
391
375 def cleaned_up_subject
392 def cleaned_up_subject
376 subject = email.subject.to_s
393 subject = email.subject.to_s
377 unless subject.respond_to?(:encoding)
394 unless subject.respond_to?(:encoding)
378 # try to reencode to utf8 manually with ruby1.8
395 # try to reencode to utf8 manually with ruby1.8
379 begin
396 begin
380 if h = email.header[:subject]
397 if h = email.header[:subject]
381 # http://tools.ietf.org/html/rfc2047#section-4
398 # http://tools.ietf.org/html/rfc2047#section-4
382 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
399 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
383 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
400 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
384 end
401 end
385 end
402 end
386 rescue
403 rescue
387 # nop
404 # nop
388 end
405 end
389 end
406 end
390 subject.strip[0,255]
407 subject.strip[0,255]
391 end
408 end
392
409
393 def self.full_sanitizer
410 def self.full_sanitizer
394 @full_sanitizer ||= HTML::FullSanitizer.new
411 @full_sanitizer ||= HTML::FullSanitizer.new
395 end
412 end
396
413
397 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
414 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
398 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
415 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
399 value = value.to_s.slice(0, limit)
416 value = value.to_s.slice(0, limit)
400 object.send("#{attribute}=", value)
417 object.send("#{attribute}=", value)
401 end
418 end
402
419
403 # Returns a User from an email address and a full name
420 # Returns a User from an email address and a full name
404 def self.new_user_from_attributes(email_address, fullname=nil)
421 def self.new_user_from_attributes(email_address, fullname=nil)
405 user = User.new
422 user = User.new
406
423
407 # Truncating the email address would result in an invalid format
424 # Truncating the email address would result in an invalid format
408 user.mail = email_address
425 user.mail = email_address
409 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
426 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
410
427
411 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
428 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
412 assign_string_attribute_with_limit(user, 'firstname', names.shift)
429 assign_string_attribute_with_limit(user, 'firstname', names.shift)
413 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
430 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
414 user.lastname = '-' if user.lastname.blank?
431 user.lastname = '-' if user.lastname.blank?
415
432
416 password_length = [Setting.password_min_length.to_i, 10].max
433 password_length = [Setting.password_min_length.to_i, 10].max
417 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
434 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
418 user.language = Setting.default_language
435 user.language = Setting.default_language
419
436
420 unless user.valid?
437 unless user.valid?
421 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
438 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
422 user.firstname = "-" unless user.errors[:firstname].blank?
439 user.firstname = "-" unless user.errors[:firstname].blank?
423 user.lastname = "-" unless user.errors[:lastname].blank?
440 user.lastname = "-" unless user.errors[:lastname].blank?
424 end
441 end
425
442
426 user
443 user
427 end
444 end
428
445
429 # Creates a User for the +email+ sender
446 # Creates a User for the +email+ sender
430 # Returns the user or nil if it could not be created
447 # Returns the user or nil if it could not be created
431 def create_user_from_email
448 def create_user_from_email
432 from = email.header['from'].to_s
449 from = email.header['from'].to_s
433 addr, name = from, nil
450 addr, name = from, nil
434 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
451 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
435 addr, name = m[2], m[1]
452 addr, name = m[2], m[1]
436 end
453 end
437 if addr.present?
454 if addr.present?
438 user = self.class.new_user_from_attributes(addr, name)
455 user = self.class.new_user_from_attributes(addr, name)
439 if user.save
456 if user.save
440 user
457 user
441 else
458 else
442 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
459 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
443 nil
460 nil
444 end
461 end
445 else
462 else
446 logger.error "MailHandler: failed to create User: no FROM address found" if logger
463 logger.error "MailHandler: failed to create User: no FROM address found" if logger
447 nil
464 nil
448 end
465 end
449 end
466 end
450
467
451 # Removes the email body of text after the truncation configurations.
468 # Removes the email body of text after the truncation configurations.
452 def cleanup_body(body)
469 def cleanup_body(body)
453 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
470 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
454 unless delimiters.empty?
471 unless delimiters.empty?
455 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
472 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
456 body = body.gsub(regex, '')
473 body = body.gsub(regex, '')
457 end
474 end
458 body.strip
475 body.strip
459 end
476 end
460
477
461 def find_assignee_from_keyword(keyword, issue)
478 def find_assignee_from_keyword(keyword, issue)
462 keyword = keyword.to_s.downcase
479 keyword = keyword.to_s.downcase
463 assignable = issue.assignable_users
480 assignable = issue.assignable_users
464 assignee = nil
481 assignee = nil
465 assignee ||= assignable.detect {|a|
482 assignee ||= assignable.detect {|a|
466 a.mail.to_s.downcase == keyword ||
483 a.mail.to_s.downcase == keyword ||
467 a.login.to_s.downcase == keyword
484 a.login.to_s.downcase == keyword
468 }
485 }
469 if assignee.nil? && keyword.match(/ /)
486 if assignee.nil? && keyword.match(/ /)
470 firstname, lastname = *(keyword.split) # "First Last Throwaway"
487 firstname, lastname = *(keyword.split) # "First Last Throwaway"
471 assignee ||= assignable.detect {|a|
488 assignee ||= assignable.detect {|a|
472 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
489 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
473 a.lastname.to_s.downcase == lastname
490 a.lastname.to_s.downcase == lastname
474 }
491 }
475 end
492 end
476 if assignee.nil?
493 if assignee.nil?
477 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
494 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
478 end
495 end
479 assignee
496 assignee
480 end
497 end
481 end
498 end
@@ -1,720 +1,794
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class MailHandlerTest < ActiveSupport::TestCase
22 class MailHandlerTest < ActiveSupport::TestCase
23 fixtures :users, :projects, :enabled_modules, :roles,
23 fixtures :users, :projects, :enabled_modules, :roles,
24 :members, :member_roles, :users,
24 :members, :member_roles, :users,
25 :issues, :issue_statuses,
25 :issues, :issue_statuses,
26 :workflows, :trackers, :projects_trackers,
26 :workflows, :trackers, :projects_trackers,
27 :versions, :enumerations, :issue_categories,
27 :versions, :enumerations, :issue_categories,
28 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
28 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
29 :boards, :messages
29 :boards, :messages
30
30
31 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
31 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
32
32
33 def setup
33 def setup
34 ActionMailer::Base.deliveries.clear
34 ActionMailer::Base.deliveries.clear
35 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
35 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
36 end
36 end
37
37
38 def teardown
38 def teardown
39 Setting.clear_cache
39 Setting.clear_cache
40 end
40 end
41
41
42 def test_add_issue
42 def test_add_issue
43 ActionMailer::Base.deliveries.clear
43 ActionMailer::Base.deliveries.clear
44 # This email contains: 'Project: onlinestore'
44 # This email contains: 'Project: onlinestore'
45 issue = submit_email('ticket_on_given_project.eml')
45 issue = submit_email('ticket_on_given_project.eml')
46 assert issue.is_a?(Issue)
46 assert issue.is_a?(Issue)
47 assert !issue.new_record?
47 assert !issue.new_record?
48 issue.reload
48 issue.reload
49 assert_equal Project.find(2), issue.project
49 assert_equal Project.find(2), issue.project
50 assert_equal issue.project.trackers.first, issue.tracker
50 assert_equal issue.project.trackers.first, issue.tracker
51 assert_equal 'New ticket on a given project', issue.subject
51 assert_equal 'New ticket on a given project', issue.subject
52 assert_equal User.find_by_login('jsmith'), issue.author
52 assert_equal User.find_by_login('jsmith'), issue.author
53 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
53 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
54 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
54 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
55 assert_equal '2010-01-01', issue.start_date.to_s
55 assert_equal '2010-01-01', issue.start_date.to_s
56 assert_equal '2010-12-31', issue.due_date.to_s
56 assert_equal '2010-12-31', issue.due_date.to_s
57 assert_equal User.find_by_login('jsmith'), issue.assigned_to
57 assert_equal User.find_by_login('jsmith'), issue.assigned_to
58 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
58 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
59 assert_equal 2.5, issue.estimated_hours
59 assert_equal 2.5, issue.estimated_hours
60 assert_equal 30, issue.done_ratio
60 assert_equal 30, issue.done_ratio
61 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
61 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
62 # keywords should be removed from the email body
62 # keywords should be removed from the email body
63 assert !issue.description.match(/^Project:/i)
63 assert !issue.description.match(/^Project:/i)
64 assert !issue.description.match(/^Status:/i)
64 assert !issue.description.match(/^Status:/i)
65 assert !issue.description.match(/^Start Date:/i)
65 assert !issue.description.match(/^Start Date:/i)
66 # Email notification should be sent
66 # Email notification should be sent
67 mail = ActionMailer::Base.deliveries.last
67 mail = ActionMailer::Base.deliveries.last
68 assert_not_nil mail
68 assert_not_nil mail
69 assert mail.subject.include?('New ticket on a given project')
69 assert mail.subject.include?('New ticket on a given project')
70 end
70 end
71
71
72 def test_add_issue_with_default_tracker
72 def test_add_issue_with_default_tracker
73 # This email contains: 'Project: onlinestore'
73 # This email contains: 'Project: onlinestore'
74 issue = submit_email(
74 issue = submit_email(
75 'ticket_on_given_project.eml',
75 'ticket_on_given_project.eml',
76 :issue => {:tracker => 'Support request'}
76 :issue => {:tracker => 'Support request'}
77 )
77 )
78 assert issue.is_a?(Issue)
78 assert issue.is_a?(Issue)
79 assert !issue.new_record?
79 assert !issue.new_record?
80 issue.reload
80 issue.reload
81 assert_equal 'Support request', issue.tracker.name
81 assert_equal 'Support request', issue.tracker.name
82 end
82 end
83
83
84 def test_add_issue_with_status
84 def test_add_issue_with_status
85 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
85 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
86 issue = submit_email('ticket_on_given_project.eml')
86 issue = submit_email('ticket_on_given_project.eml')
87 assert issue.is_a?(Issue)
87 assert issue.is_a?(Issue)
88 assert !issue.new_record?
88 assert !issue.new_record?
89 issue.reload
89 issue.reload
90 assert_equal Project.find(2), issue.project
90 assert_equal Project.find(2), issue.project
91 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
91 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
92 end
92 end
93
93
94 def test_add_issue_with_attributes_override
94 def test_add_issue_with_attributes_override
95 issue = submit_email(
95 issue = submit_email(
96 'ticket_with_attributes.eml',
96 'ticket_with_attributes.eml',
97 :allow_override => 'tracker,category,priority'
97 :allow_override => 'tracker,category,priority'
98 )
98 )
99 assert issue.is_a?(Issue)
99 assert issue.is_a?(Issue)
100 assert !issue.new_record?
100 assert !issue.new_record?
101 issue.reload
101 issue.reload
102 assert_equal 'New ticket on a given project', issue.subject
102 assert_equal 'New ticket on a given project', issue.subject
103 assert_equal User.find_by_login('jsmith'), issue.author
103 assert_equal User.find_by_login('jsmith'), issue.author
104 assert_equal Project.find(2), issue.project
104 assert_equal Project.find(2), issue.project
105 assert_equal 'Feature request', issue.tracker.to_s
105 assert_equal 'Feature request', issue.tracker.to_s
106 assert_equal 'Stock management', issue.category.to_s
106 assert_equal 'Stock management', issue.category.to_s
107 assert_equal 'Urgent', issue.priority.to_s
107 assert_equal 'Urgent', issue.priority.to_s
108 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
108 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
109 end
109 end
110
110
111 def test_add_issue_with_group_assignment
111 def test_add_issue_with_group_assignment
112 with_settings :issue_group_assignment => '1' do
112 with_settings :issue_group_assignment => '1' do
113 issue = submit_email('ticket_on_given_project.eml') do |email|
113 issue = submit_email('ticket_on_given_project.eml') do |email|
114 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
114 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
115 end
115 end
116 assert issue.is_a?(Issue)
116 assert issue.is_a?(Issue)
117 assert !issue.new_record?
117 assert !issue.new_record?
118 issue.reload
118 issue.reload
119 assert_equal Group.find(11), issue.assigned_to
119 assert_equal Group.find(11), issue.assigned_to
120 end
120 end
121 end
121 end
122
122
123 def test_add_issue_with_partial_attributes_override
123 def test_add_issue_with_partial_attributes_override
124 issue = submit_email(
124 issue = submit_email(
125 'ticket_with_attributes.eml',
125 'ticket_with_attributes.eml',
126 :issue => {:priority => 'High'},
126 :issue => {:priority => 'High'},
127 :allow_override => ['tracker']
127 :allow_override => ['tracker']
128 )
128 )
129 assert issue.is_a?(Issue)
129 assert issue.is_a?(Issue)
130 assert !issue.new_record?
130 assert !issue.new_record?
131 issue.reload
131 issue.reload
132 assert_equal 'New ticket on a given project', issue.subject
132 assert_equal 'New ticket on a given project', issue.subject
133 assert_equal User.find_by_login('jsmith'), issue.author
133 assert_equal User.find_by_login('jsmith'), issue.author
134 assert_equal Project.find(2), issue.project
134 assert_equal Project.find(2), issue.project
135 assert_equal 'Feature request', issue.tracker.to_s
135 assert_equal 'Feature request', issue.tracker.to_s
136 assert_nil issue.category
136 assert_nil issue.category
137 assert_equal 'High', issue.priority.to_s
137 assert_equal 'High', issue.priority.to_s
138 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
138 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
139 end
139 end
140
140
141 def test_add_issue_with_spaces_between_attribute_and_separator
141 def test_add_issue_with_spaces_between_attribute_and_separator
142 issue = submit_email(
142 issue = submit_email(
143 'ticket_with_spaces_between_attribute_and_separator.eml',
143 'ticket_with_spaces_between_attribute_and_separator.eml',
144 :allow_override => 'tracker,category,priority'
144 :allow_override => 'tracker,category,priority'
145 )
145 )
146 assert issue.is_a?(Issue)
146 assert issue.is_a?(Issue)
147 assert !issue.new_record?
147 assert !issue.new_record?
148 issue.reload
148 issue.reload
149 assert_equal 'New ticket on a given project', issue.subject
149 assert_equal 'New ticket on a given project', issue.subject
150 assert_equal User.find_by_login('jsmith'), issue.author
150 assert_equal User.find_by_login('jsmith'), issue.author
151 assert_equal Project.find(2), issue.project
151 assert_equal Project.find(2), issue.project
152 assert_equal 'Feature request', issue.tracker.to_s
152 assert_equal 'Feature request', issue.tracker.to_s
153 assert_equal 'Stock management', issue.category.to_s
153 assert_equal 'Stock management', issue.category.to_s
154 assert_equal 'Urgent', issue.priority.to_s
154 assert_equal 'Urgent', issue.priority.to_s
155 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
155 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
156 end
156 end
157
157
158 def test_add_issue_with_attachment_to_specific_project
158 def test_add_issue_with_attachment_to_specific_project
159 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
159 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
160 assert issue.is_a?(Issue)
160 assert issue.is_a?(Issue)
161 assert !issue.new_record?
161 assert !issue.new_record?
162 issue.reload
162 issue.reload
163 assert_equal 'Ticket created by email with attachment', issue.subject
163 assert_equal 'Ticket created by email with attachment', issue.subject
164 assert_equal User.find_by_login('jsmith'), issue.author
164 assert_equal User.find_by_login('jsmith'), issue.author
165 assert_equal Project.find(2), issue.project
165 assert_equal Project.find(2), issue.project
166 assert_equal 'This is a new ticket with attachments', issue.description
166 assert_equal 'This is a new ticket with attachments', issue.description
167 # Attachment properties
167 # Attachment properties
168 assert_equal 1, issue.attachments.size
168 assert_equal 1, issue.attachments.size
169 assert_equal 'Paella.jpg', issue.attachments.first.filename
169 assert_equal 'Paella.jpg', issue.attachments.first.filename
170 assert_equal 'image/jpeg', issue.attachments.first.content_type
170 assert_equal 'image/jpeg', issue.attachments.first.content_type
171 assert_equal 10790, issue.attachments.first.filesize
171 assert_equal 10790, issue.attachments.first.filesize
172 end
172 end
173
173
174 def test_add_issue_with_custom_fields
174 def test_add_issue_with_custom_fields
175 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
175 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
176 assert issue.is_a?(Issue)
176 assert issue.is_a?(Issue)
177 assert !issue.new_record?
177 assert !issue.new_record?
178 issue.reload
178 issue.reload
179 assert_equal 'New ticket with custom field values', issue.subject
179 assert_equal 'New ticket with custom field values', issue.subject
180 assert_equal 'Value for a custom field',
180 assert_equal 'Value for a custom field',
181 issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
181 issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
182 assert !issue.description.match(/^searchable field:/i)
182 assert !issue.description.match(/^searchable field:/i)
183 end
183 end
184
184
185 def test_add_issue_with_version_custom_fields
185 def test_add_issue_with_version_custom_fields
186 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
186 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
187
187
188 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email|
188 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email|
189 email << "Affected version: 1.0\n"
189 email << "Affected version: 1.0\n"
190 end
190 end
191 assert issue.is_a?(Issue)
191 assert issue.is_a?(Issue)
192 assert !issue.new_record?
192 assert !issue.new_record?
193 issue.reload
193 issue.reload
194 assert_equal '2', issue.custom_field_value(field)
194 assert_equal '2', issue.custom_field_value(field)
195 end
195 end
196
196
197 def test_add_issue_should_match_assignee_on_display_name
197 def test_add_issue_should_match_assignee_on_display_name
198 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
198 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
199 User.add_to_project(user, Project.find(2))
199 User.add_to_project(user, Project.find(2))
200 issue = submit_email('ticket_on_given_project.eml') do |email|
200 issue = submit_email('ticket_on_given_project.eml') do |email|
201 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
201 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
202 end
202 end
203 assert issue.is_a?(Issue)
203 assert issue.is_a?(Issue)
204 assert_equal user, issue.assigned_to
204 assert_equal user, issue.assigned_to
205 end
205 end
206
206
207 def test_add_issue_with_cc
207 def test_add_issue_with_cc
208 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
208 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
209 assert issue.is_a?(Issue)
209 assert issue.is_a?(Issue)
210 assert !issue.new_record?
210 assert !issue.new_record?
211 issue.reload
211 issue.reload
212 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
212 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
213 assert_equal 1, issue.watcher_user_ids.size
213 assert_equal 1, issue.watcher_user_ids.size
214 end
214 end
215
215
216 def test_add_issue_by_unknown_user
216 def test_add_issue_by_unknown_user
217 assert_no_difference 'User.count' do
217 assert_no_difference 'User.count' do
218 assert_equal false,
218 assert_equal false,
219 submit_email(
219 submit_email(
220 'ticket_by_unknown_user.eml',
220 'ticket_by_unknown_user.eml',
221 :issue => {:project => 'ecookbook'}
221 :issue => {:project => 'ecookbook'}
222 )
222 )
223 end
223 end
224 end
224 end
225
225
226 def test_add_issue_by_anonymous_user
226 def test_add_issue_by_anonymous_user
227 Role.anonymous.add_permission!(:add_issues)
227 Role.anonymous.add_permission!(:add_issues)
228 assert_no_difference 'User.count' do
228 assert_no_difference 'User.count' do
229 issue = submit_email(
229 issue = submit_email(
230 'ticket_by_unknown_user.eml',
230 'ticket_by_unknown_user.eml',
231 :issue => {:project => 'ecookbook'},
231 :issue => {:project => 'ecookbook'},
232 :unknown_user => 'accept'
232 :unknown_user => 'accept'
233 )
233 )
234 assert issue.is_a?(Issue)
234 assert issue.is_a?(Issue)
235 assert issue.author.anonymous?
235 assert issue.author.anonymous?
236 end
236 end
237 end
237 end
238
238
239 def test_add_issue_by_anonymous_user_with_no_from_address
239 def test_add_issue_by_anonymous_user_with_no_from_address
240 Role.anonymous.add_permission!(:add_issues)
240 Role.anonymous.add_permission!(:add_issues)
241 assert_no_difference 'User.count' do
241 assert_no_difference 'User.count' do
242 issue = submit_email(
242 issue = submit_email(
243 'ticket_by_empty_user.eml',
243 'ticket_by_empty_user.eml',
244 :issue => {:project => 'ecookbook'},
244 :issue => {:project => 'ecookbook'},
245 :unknown_user => 'accept'
245 :unknown_user => 'accept'
246 )
246 )
247 assert issue.is_a?(Issue)
247 assert issue.is_a?(Issue)
248 assert issue.author.anonymous?
248 assert issue.author.anonymous?
249 end
249 end
250 end
250 end
251
251
252 def test_add_issue_by_anonymous_user_on_private_project
252 def test_add_issue_by_anonymous_user_on_private_project
253 Role.anonymous.add_permission!(:add_issues)
253 Role.anonymous.add_permission!(:add_issues)
254 assert_no_difference 'User.count' do
254 assert_no_difference 'User.count' do
255 assert_no_difference 'Issue.count' do
255 assert_no_difference 'Issue.count' do
256 assert_equal false,
256 assert_equal false,
257 submit_email(
257 submit_email(
258 'ticket_by_unknown_user.eml',
258 'ticket_by_unknown_user.eml',
259 :issue => {:project => 'onlinestore'},
259 :issue => {:project => 'onlinestore'},
260 :unknown_user => 'accept'
260 :unknown_user => 'accept'
261 )
261 )
262 end
262 end
263 end
263 end
264 end
264 end
265
265
266 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
266 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
267 assert_no_difference 'User.count' do
267 assert_no_difference 'User.count' do
268 assert_difference 'Issue.count' do
268 assert_difference 'Issue.count' do
269 issue = submit_email(
269 issue = submit_email(
270 'ticket_by_unknown_user.eml',
270 'ticket_by_unknown_user.eml',
271 :issue => {:project => 'onlinestore'},
271 :issue => {:project => 'onlinestore'},
272 :no_permission_check => '1',
272 :no_permission_check => '1',
273 :unknown_user => 'accept'
273 :unknown_user => 'accept'
274 )
274 )
275 assert issue.is_a?(Issue)
275 assert issue.is_a?(Issue)
276 assert issue.author.anonymous?
276 assert issue.author.anonymous?
277 assert !issue.project.is_public?
277 assert !issue.project.is_public?
278 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
278 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
279 end
279 end
280 end
280 end
281 end
281 end
282
282
283 def test_add_issue_by_created_user
283 def test_add_issue_by_created_user
284 Setting.default_language = 'en'
284 Setting.default_language = 'en'
285 assert_difference 'User.count' do
285 assert_difference 'User.count' do
286 issue = submit_email(
286 issue = submit_email(
287 'ticket_by_unknown_user.eml',
287 'ticket_by_unknown_user.eml',
288 :issue => {:project => 'ecookbook'},
288 :issue => {:project => 'ecookbook'},
289 :unknown_user => 'create'
289 :unknown_user => 'create'
290 )
290 )
291 assert issue.is_a?(Issue)
291 assert issue.is_a?(Issue)
292 assert issue.author.active?
292 assert issue.author.active?
293 assert_equal 'john.doe@somenet.foo', issue.author.mail
293 assert_equal 'john.doe@somenet.foo', issue.author.mail
294 assert_equal 'John', issue.author.firstname
294 assert_equal 'John', issue.author.firstname
295 assert_equal 'Doe', issue.author.lastname
295 assert_equal 'Doe', issue.author.lastname
296
296
297 # account information
297 # account information
298 email = ActionMailer::Base.deliveries.first
298 email = ActionMailer::Base.deliveries.first
299 assert_not_nil email
299 assert_not_nil email
300 assert email.subject.include?('account activation')
300 assert email.subject.include?('account activation')
301 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
301 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
302 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
302 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
303 assert_equal issue.author, User.try_to_login(login, password)
303 assert_equal issue.author, User.try_to_login(login, password)
304 end
304 end
305 end
305 end
306
306
307 def test_add_issue_without_from_header
307 def test_add_issue_without_from_header
308 Role.anonymous.add_permission!(:add_issues)
308 Role.anonymous.add_permission!(:add_issues)
309 assert_equal false, submit_email('ticket_without_from_header.eml')
309 assert_equal false, submit_email('ticket_without_from_header.eml')
310 end
310 end
311
311
312 def test_add_issue_with_invalid_attributes
312 def test_add_issue_with_invalid_attributes
313 issue = submit_email(
313 issue = submit_email(
314 'ticket_with_invalid_attributes.eml',
314 'ticket_with_invalid_attributes.eml',
315 :allow_override => 'tracker,category,priority'
315 :allow_override => 'tracker,category,priority'
316 )
316 )
317 assert issue.is_a?(Issue)
317 assert issue.is_a?(Issue)
318 assert !issue.new_record?
318 assert !issue.new_record?
319 issue.reload
319 issue.reload
320 assert_nil issue.assigned_to
320 assert_nil issue.assigned_to
321 assert_nil issue.start_date
321 assert_nil issue.start_date
322 assert_nil issue.due_date
322 assert_nil issue.due_date
323 assert_equal 0, issue.done_ratio
323 assert_equal 0, issue.done_ratio
324 assert_equal 'Normal', issue.priority.to_s
324 assert_equal 'Normal', issue.priority.to_s
325 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
325 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
326 end
326 end
327
327
328 def test_add_issue_with_localized_attributes
328 def test_add_issue_with_localized_attributes
329 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
329 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
330 issue = submit_email(
330 issue = submit_email(
331 'ticket_with_localized_attributes.eml',
331 'ticket_with_localized_attributes.eml',
332 :allow_override => 'tracker,category,priority'
332 :allow_override => 'tracker,category,priority'
333 )
333 )
334 assert issue.is_a?(Issue)
334 assert issue.is_a?(Issue)
335 assert !issue.new_record?
335 assert !issue.new_record?
336 issue.reload
336 issue.reload
337 assert_equal 'New ticket on a given project', issue.subject
337 assert_equal 'New ticket on a given project', issue.subject
338 assert_equal User.find_by_login('jsmith'), issue.author
338 assert_equal User.find_by_login('jsmith'), issue.author
339 assert_equal Project.find(2), issue.project
339 assert_equal Project.find(2), issue.project
340 assert_equal 'Feature request', issue.tracker.to_s
340 assert_equal 'Feature request', issue.tracker.to_s
341 assert_equal 'Stock management', issue.category.to_s
341 assert_equal 'Stock management', issue.category.to_s
342 assert_equal 'Urgent', issue.priority.to_s
342 assert_equal 'Urgent', issue.priority.to_s
343 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
343 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
344 end
344 end
345
345
346 def test_add_issue_with_japanese_keywords
346 def test_add_issue_with_japanese_keywords
347 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
347 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
348 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
348 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
349 tracker = Tracker.create!(:name => ja_dev)
349 tracker = Tracker.create!(:name => ja_dev)
350 Project.find(1).trackers << tracker
350 Project.find(1).trackers << tracker
351 issue = submit_email(
351 issue = submit_email(
352 'japanese_keywords_iso_2022_jp.eml',
352 'japanese_keywords_iso_2022_jp.eml',
353 :issue => {:project => 'ecookbook'},
353 :issue => {:project => 'ecookbook'},
354 :allow_override => 'tracker'
354 :allow_override => 'tracker'
355 )
355 )
356 assert_kind_of Issue, issue
356 assert_kind_of Issue, issue
357 assert_equal tracker, issue.tracker
357 assert_equal tracker, issue.tracker
358 end
358 end
359
359
360 def test_add_issue_from_apple_mail
360 def test_add_issue_from_apple_mail
361 issue = submit_email(
361 issue = submit_email(
362 'apple_mail_with_attachment.eml',
362 'apple_mail_with_attachment.eml',
363 :issue => {:project => 'ecookbook'}
363 :issue => {:project => 'ecookbook'}
364 )
364 )
365 assert_kind_of Issue, issue
365 assert_kind_of Issue, issue
366 assert_equal 1, issue.attachments.size
366 assert_equal 1, issue.attachments.size
367
367
368 attachment = issue.attachments.first
368 attachment = issue.attachments.first
369 assert_equal 'paella.jpg', attachment.filename
369 assert_equal 'paella.jpg', attachment.filename
370 assert_equal 10790, attachment.filesize
370 assert_equal 10790, attachment.filesize
371 assert File.exist?(attachment.diskfile)
371 assert File.exist?(attachment.diskfile)
372 assert_equal 10790, File.size(attachment.diskfile)
372 assert_equal 10790, File.size(attachment.diskfile)
373 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
373 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
374 end
374 end
375
375
376 def test_thunderbird_with_attachment_ja
377 issue = submit_email(
378 'thunderbird_with_attachment_ja.eml',
379 :issue => {:project => 'ecookbook'}
380 )
381 assert_kind_of Issue, issue
382 assert_equal 1, issue.attachments.size
383 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
384 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
385 attachment = issue.attachments.first
386 assert_equal ja, attachment.filename
387 assert_equal 5, attachment.filesize
388 assert File.exist?(attachment.diskfile)
389 assert_equal 5, File.size(attachment.diskfile)
390 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
391 end
392
393 def test_gmail_with_attachment_ja
394 issue = submit_email(
395 'gmail_with_attachment_ja.eml',
396 :issue => {:project => 'ecookbook'}
397 )
398 assert_kind_of Issue, issue
399 assert_equal 1, issue.attachments.size
400 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
401 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
402 attachment = issue.attachments.first
403 assert_equal ja, attachment.filename
404 assert_equal 5, attachment.filesize
405 assert File.exist?(attachment.diskfile)
406 assert_equal 5, File.size(attachment.diskfile)
407 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
408 end
409
410 def test_thunderbird_with_attachment_latin1
411 issue = submit_email(
412 'thunderbird_with_attachment_iso-8859-1.eml',
413 :issue => {:project => 'ecookbook'}
414 )
415 assert_kind_of Issue, issue
416 assert_equal 1, issue.attachments.size
417 u = ""
418 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
419 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
420 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
421 11.times { u << u1 }
422 attachment = issue.attachments.first
423 assert_equal "#{u}.png", attachment.filename
424 assert_equal 130, attachment.filesize
425 assert File.exist?(attachment.diskfile)
426 assert_equal 130, File.size(attachment.diskfile)
427 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
428 end
429
430 def test_gmail_with_attachment_latin1
431 issue = submit_email(
432 'gmail_with_attachment_iso-8859-1.eml',
433 :issue => {:project => 'ecookbook'}
434 )
435 assert_kind_of Issue, issue
436 assert_equal 1, issue.attachments.size
437 u = ""
438 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
439 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
440 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
441 11.times { u << u1 }
442 attachment = issue.attachments.first
443 assert_equal "#{u}.txt", attachment.filename
444 assert_equal 5, attachment.filesize
445 assert File.exist?(attachment.diskfile)
446 assert_equal 5, File.size(attachment.diskfile)
447 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
448 end
449
376 def test_add_issue_with_iso_8859_1_subject
450 def test_add_issue_with_iso_8859_1_subject
377 issue = submit_email(
451 issue = submit_email(
378 'subject_as_iso-8859-1.eml',
452 'subject_as_iso-8859-1.eml',
379 :issue => {:project => 'ecookbook'}
453 :issue => {:project => 'ecookbook'}
380 )
454 )
381 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..."
455 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..."
382 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
456 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
383 assert_kind_of Issue, issue
457 assert_kind_of Issue, issue
384 assert_equal str, issue.subject
458 assert_equal str, issue.subject
385 end
459 end
386
460
387 def test_add_issue_with_japanese_subject
461 def test_add_issue_with_japanese_subject
388 issue = submit_email(
462 issue = submit_email(
389 'subject_japanese_1.eml',
463 'subject_japanese_1.eml',
390 :issue => {:project => 'ecookbook'}
464 :issue => {:project => 'ecookbook'}
391 )
465 )
392 assert_kind_of Issue, issue
466 assert_kind_of Issue, issue
393 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
467 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
394 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
468 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
395 assert_equal ja, issue.subject
469 assert_equal ja, issue.subject
396 end
470 end
397
471
398 def test_add_issue_with_no_subject_header
472 def test_add_issue_with_no_subject_header
399 issue = submit_email(
473 issue = submit_email(
400 'no_subject_header.eml',
474 'no_subject_header.eml',
401 :issue => {:project => 'ecookbook'}
475 :issue => {:project => 'ecookbook'}
402 )
476 )
403 assert_kind_of Issue, issue
477 assert_kind_of Issue, issue
404 assert_equal '(no subject)', issue.subject
478 assert_equal '(no subject)', issue.subject
405 end
479 end
406
480
407 def test_add_issue_with_mixed_japanese_subject
481 def test_add_issue_with_mixed_japanese_subject
408 issue = submit_email(
482 issue = submit_email(
409 'subject_japanese_2.eml',
483 'subject_japanese_2.eml',
410 :issue => {:project => 'ecookbook'}
484 :issue => {:project => 'ecookbook'}
411 )
485 )
412 assert_kind_of Issue, issue
486 assert_kind_of Issue, issue
413 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
487 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
414 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
488 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
415 assert_equal ja, issue.subject
489 assert_equal ja, issue.subject
416 end
490 end
417
491
418 def test_should_ignore_emails_from_locked_users
492 def test_should_ignore_emails_from_locked_users
419 User.find(2).lock!
493 User.find(2).lock!
420
494
421 MailHandler.any_instance.expects(:dispatch).never
495 MailHandler.any_instance.expects(:dispatch).never
422 assert_no_difference 'Issue.count' do
496 assert_no_difference 'Issue.count' do
423 assert_equal false, submit_email('ticket_on_given_project.eml')
497 assert_equal false, submit_email('ticket_on_given_project.eml')
424 end
498 end
425 end
499 end
426
500
427 def test_should_ignore_emails_from_emission_address
501 def test_should_ignore_emails_from_emission_address
428 Role.anonymous.add_permission!(:add_issues)
502 Role.anonymous.add_permission!(:add_issues)
429 assert_no_difference 'User.count' do
503 assert_no_difference 'User.count' do
430 assert_equal false,
504 assert_equal false,
431 submit_email(
505 submit_email(
432 'ticket_from_emission_address.eml',
506 'ticket_from_emission_address.eml',
433 :issue => {:project => 'ecookbook'},
507 :issue => {:project => 'ecookbook'},
434 :unknown_user => 'create'
508 :unknown_user => 'create'
435 )
509 )
436 end
510 end
437 end
511 end
438
512
439 def test_should_ignore_auto_replied_emails
513 def test_should_ignore_auto_replied_emails
440 MailHandler.any_instance.expects(:dispatch).never
514 MailHandler.any_instance.expects(:dispatch).never
441 [
515 [
442 "X-Auto-Response-Suppress: OOF",
516 "X-Auto-Response-Suppress: OOF",
443 "Auto-Submitted: auto-replied",
517 "Auto-Submitted: auto-replied",
444 "Auto-Submitted: Auto-Replied",
518 "Auto-Submitted: Auto-Replied",
445 "Auto-Submitted: auto-generated"
519 "Auto-Submitted: auto-generated"
446 ].each do |header|
520 ].each do |header|
447 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
521 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
448 raw = header + "\n" + raw
522 raw = header + "\n" + raw
449
523
450 assert_no_difference 'Issue.count' do
524 assert_no_difference 'Issue.count' do
451 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
525 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
452 end
526 end
453 end
527 end
454 end
528 end
455
529
456 def test_add_issue_should_send_email_notification
530 def test_add_issue_should_send_email_notification
457 Setting.notified_events = ['issue_added']
531 Setting.notified_events = ['issue_added']
458 ActionMailer::Base.deliveries.clear
532 ActionMailer::Base.deliveries.clear
459 # This email contains: 'Project: onlinestore'
533 # This email contains: 'Project: onlinestore'
460 issue = submit_email('ticket_on_given_project.eml')
534 issue = submit_email('ticket_on_given_project.eml')
461 assert issue.is_a?(Issue)
535 assert issue.is_a?(Issue)
462 assert_equal 1, ActionMailer::Base.deliveries.size
536 assert_equal 1, ActionMailer::Base.deliveries.size
463 end
537 end
464
538
465 def test_update_issue
539 def test_update_issue
466 journal = submit_email('ticket_reply.eml')
540 journal = submit_email('ticket_reply.eml')
467 assert journal.is_a?(Journal)
541 assert journal.is_a?(Journal)
468 assert_equal User.find_by_login('jsmith'), journal.user
542 assert_equal User.find_by_login('jsmith'), journal.user
469 assert_equal Issue.find(2), journal.journalized
543 assert_equal Issue.find(2), journal.journalized
470 assert_match /This is reply/, journal.notes
544 assert_match /This is reply/, journal.notes
471 assert_equal false, journal.private_notes
545 assert_equal false, journal.private_notes
472 assert_equal 'Feature request', journal.issue.tracker.name
546 assert_equal 'Feature request', journal.issue.tracker.name
473 end
547 end
474
548
475 def test_update_issue_with_attribute_changes
549 def test_update_issue_with_attribute_changes
476 # This email contains: 'Status: Resolved'
550 # This email contains: 'Status: Resolved'
477 journal = submit_email('ticket_reply_with_status.eml')
551 journal = submit_email('ticket_reply_with_status.eml')
478 assert journal.is_a?(Journal)
552 assert journal.is_a?(Journal)
479 issue = Issue.find(journal.issue.id)
553 issue = Issue.find(journal.issue.id)
480 assert_equal User.find_by_login('jsmith'), journal.user
554 assert_equal User.find_by_login('jsmith'), journal.user
481 assert_equal Issue.find(2), journal.journalized
555 assert_equal Issue.find(2), journal.journalized
482 assert_match /This is reply/, journal.notes
556 assert_match /This is reply/, journal.notes
483 assert_equal 'Feature request', journal.issue.tracker.name
557 assert_equal 'Feature request', journal.issue.tracker.name
484 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
558 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
485 assert_equal '2010-01-01', issue.start_date.to_s
559 assert_equal '2010-01-01', issue.start_date.to_s
486 assert_equal '2010-12-31', issue.due_date.to_s
560 assert_equal '2010-12-31', issue.due_date.to_s
487 assert_equal User.find_by_login('jsmith'), issue.assigned_to
561 assert_equal User.find_by_login('jsmith'), issue.assigned_to
488 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
562 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
489 # keywords should be removed from the email body
563 # keywords should be removed from the email body
490 assert !journal.notes.match(/^Status:/i)
564 assert !journal.notes.match(/^Status:/i)
491 assert !journal.notes.match(/^Start Date:/i)
565 assert !journal.notes.match(/^Start Date:/i)
492 end
566 end
493
567
494 def test_update_issue_with_attachment
568 def test_update_issue_with_attachment
495 assert_difference 'Journal.count' do
569 assert_difference 'Journal.count' do
496 assert_difference 'JournalDetail.count' do
570 assert_difference 'JournalDetail.count' do
497 assert_difference 'Attachment.count' do
571 assert_difference 'Attachment.count' do
498 assert_no_difference 'Issue.count' do
572 assert_no_difference 'Issue.count' do
499 journal = submit_email('ticket_with_attachment.eml') do |raw|
573 journal = submit_email('ticket_with_attachment.eml') do |raw|
500 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
574 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
501 end
575 end
502 end
576 end
503 end
577 end
504 end
578 end
505 end
579 end
506 journal = Journal.first(:order => 'id DESC')
580 journal = Journal.first(:order => 'id DESC')
507 assert_equal Issue.find(2), journal.journalized
581 assert_equal Issue.find(2), journal.journalized
508 assert_equal 1, journal.details.size
582 assert_equal 1, journal.details.size
509
583
510 detail = journal.details.first
584 detail = journal.details.first
511 assert_equal 'attachment', detail.property
585 assert_equal 'attachment', detail.property
512 assert_equal 'Paella.jpg', detail.value
586 assert_equal 'Paella.jpg', detail.value
513 end
587 end
514
588
515 def test_update_issue_should_send_email_notification
589 def test_update_issue_should_send_email_notification
516 ActionMailer::Base.deliveries.clear
590 ActionMailer::Base.deliveries.clear
517 journal = submit_email('ticket_reply.eml')
591 journal = submit_email('ticket_reply.eml')
518 assert journal.is_a?(Journal)
592 assert journal.is_a?(Journal)
519 assert_equal 1, ActionMailer::Base.deliveries.size
593 assert_equal 1, ActionMailer::Base.deliveries.size
520 end
594 end
521
595
522 def test_update_issue_should_not_set_defaults
596 def test_update_issue_should_not_set_defaults
523 journal = submit_email(
597 journal = submit_email(
524 'ticket_reply.eml',
598 'ticket_reply.eml',
525 :issue => {:tracker => 'Support request', :priority => 'High'}
599 :issue => {:tracker => 'Support request', :priority => 'High'}
526 )
600 )
527 assert journal.is_a?(Journal)
601 assert journal.is_a?(Journal)
528 assert_match /This is reply/, journal.notes
602 assert_match /This is reply/, journal.notes
529 assert_equal 'Feature request', journal.issue.tracker.name
603 assert_equal 'Feature request', journal.issue.tracker.name
530 assert_equal 'Normal', journal.issue.priority.name
604 assert_equal 'Normal', journal.issue.priority.name
531 end
605 end
532
606
533 def test_replying_to_a_private_note_should_add_reply_as_private
607 def test_replying_to_a_private_note_should_add_reply_as_private
534 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
608 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
535
609
536 assert_difference 'Journal.count' do
610 assert_difference 'Journal.count' do
537 journal = submit_email('ticket_reply.eml') do |email|
611 journal = submit_email('ticket_reply.eml') do |email|
538 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
612 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
539 end
613 end
540
614
541 assert_kind_of Journal, journal
615 assert_kind_of Journal, journal
542 assert_match /This is reply/, journal.notes
616 assert_match /This is reply/, journal.notes
543 assert_equal true, journal.private_notes
617 assert_equal true, journal.private_notes
544 end
618 end
545 end
619 end
546
620
547 def test_reply_to_a_message
621 def test_reply_to_a_message
548 m = submit_email('message_reply.eml')
622 m = submit_email('message_reply.eml')
549 assert m.is_a?(Message)
623 assert m.is_a?(Message)
550 assert !m.new_record?
624 assert !m.new_record?
551 m.reload
625 m.reload
552 assert_equal 'Reply via email', m.subject
626 assert_equal 'Reply via email', m.subject
553 # The email replies to message #2 which is part of the thread of message #1
627 # The email replies to message #2 which is part of the thread of message #1
554 assert_equal Message.find(1), m.parent
628 assert_equal Message.find(1), m.parent
555 end
629 end
556
630
557 def test_reply_to_a_message_by_subject
631 def test_reply_to_a_message_by_subject
558 m = submit_email('message_reply_by_subject.eml')
632 m = submit_email('message_reply_by_subject.eml')
559 assert m.is_a?(Message)
633 assert m.is_a?(Message)
560 assert !m.new_record?
634 assert !m.new_record?
561 m.reload
635 m.reload
562 assert_equal 'Reply to the first post', m.subject
636 assert_equal 'Reply to the first post', m.subject
563 assert_equal Message.find(1), m.parent
637 assert_equal Message.find(1), m.parent
564 end
638 end
565
639
566 def test_should_strip_tags_of_html_only_emails
640 def test_should_strip_tags_of_html_only_emails
567 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
641 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
568 assert issue.is_a?(Issue)
642 assert issue.is_a?(Issue)
569 assert !issue.new_record?
643 assert !issue.new_record?
570 issue.reload
644 issue.reload
571 assert_equal 'HTML email', issue.subject
645 assert_equal 'HTML email', issue.subject
572 assert_equal 'This is a html-only email.', issue.description
646 assert_equal 'This is a html-only email.', issue.description
573 end
647 end
574
648
575 context "truncate emails based on the Setting" do
649 context "truncate emails based on the Setting" do
576 context "with no setting" do
650 context "with no setting" do
577 setup do
651 setup do
578 Setting.mail_handler_body_delimiters = ''
652 Setting.mail_handler_body_delimiters = ''
579 end
653 end
580
654
581 should "add the entire email into the issue" do
655 should "add the entire email into the issue" do
582 issue = submit_email('ticket_on_given_project.eml')
656 issue = submit_email('ticket_on_given_project.eml')
583 assert_issue_created(issue)
657 assert_issue_created(issue)
584 assert issue.description.include?('---')
658 assert issue.description.include?('---')
585 assert issue.description.include?('This paragraph is after the delimiter')
659 assert issue.description.include?('This paragraph is after the delimiter')
586 end
660 end
587 end
661 end
588
662
589 context "with a single string" do
663 context "with a single string" do
590 setup do
664 setup do
591 Setting.mail_handler_body_delimiters = '---'
665 Setting.mail_handler_body_delimiters = '---'
592 end
666 end
593 should "truncate the email at the delimiter for the issue" do
667 should "truncate the email at the delimiter for the issue" do
594 issue = submit_email('ticket_on_given_project.eml')
668 issue = submit_email('ticket_on_given_project.eml')
595 assert_issue_created(issue)
669 assert_issue_created(issue)
596 assert issue.description.include?('This paragraph is before delimiters')
670 assert issue.description.include?('This paragraph is before delimiters')
597 assert issue.description.include?('--- This line starts with a delimiter')
671 assert issue.description.include?('--- This line starts with a delimiter')
598 assert !issue.description.match(/^---$/)
672 assert !issue.description.match(/^---$/)
599 assert !issue.description.include?('This paragraph is after the delimiter')
673 assert !issue.description.include?('This paragraph is after the delimiter')
600 end
674 end
601 end
675 end
602
676
603 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
677 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
604 setup do
678 setup do
605 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
679 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
606 end
680 end
607 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
681 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
608 journal = submit_email('issue_update_with_quoted_reply_above.eml')
682 journal = submit_email('issue_update_with_quoted_reply_above.eml')
609 assert journal.is_a?(Journal)
683 assert journal.is_a?(Journal)
610 assert journal.notes.include?('An update to the issue by the sender.')
684 assert journal.notes.include?('An update to the issue by the sender.')
611 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
685 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
612 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
686 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
613 end
687 end
614 end
688 end
615
689
616 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
690 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
617 setup do
691 setup do
618 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
692 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
619 end
693 end
620 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
694 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
621 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
695 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
622 assert journal.is_a?(Journal)
696 assert journal.is_a?(Journal)
623 assert journal.notes.include?('An update to the issue by the sender.')
697 assert journal.notes.include?('An update to the issue by the sender.')
624 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
698 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
625 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
699 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
626 end
700 end
627 end
701 end
628
702
629 context "with multiple strings" do
703 context "with multiple strings" do
630 setup do
704 setup do
631 Setting.mail_handler_body_delimiters = "---\nBREAK"
705 Setting.mail_handler_body_delimiters = "---\nBREAK"
632 end
706 end
633 should "truncate the email at the first delimiter found (BREAK)" do
707 should "truncate the email at the first delimiter found (BREAK)" do
634 issue = submit_email('ticket_on_given_project.eml')
708 issue = submit_email('ticket_on_given_project.eml')
635 assert_issue_created(issue)
709 assert_issue_created(issue)
636 assert issue.description.include?('This paragraph is before delimiters')
710 assert issue.description.include?('This paragraph is before delimiters')
637 assert !issue.description.include?('BREAK')
711 assert !issue.description.include?('BREAK')
638 assert !issue.description.include?('This paragraph is between delimiters')
712 assert !issue.description.include?('This paragraph is between delimiters')
639 assert !issue.description.match(/^---$/)
713 assert !issue.description.match(/^---$/)
640 assert !issue.description.include?('This paragraph is after the delimiter')
714 assert !issue.description.include?('This paragraph is after the delimiter')
641 end
715 end
642 end
716 end
643 end
717 end
644
718
645 def test_email_with_long_subject_line
719 def test_email_with_long_subject_line
646 issue = submit_email('ticket_with_long_subject.eml')
720 issue = submit_email('ticket_with_long_subject.eml')
647 assert issue.is_a?(Issue)
721 assert issue.is_a?(Issue)
648 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]
722 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]
649 end
723 end
650
724
651 def test_new_user_from_attributes_should_return_valid_user
725 def test_new_user_from_attributes_should_return_valid_user
652 to_test = {
726 to_test = {
653 # [address, name] => [login, firstname, lastname]
727 # [address, name] => [login, firstname, lastname]
654 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
728 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
655 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
729 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
656 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
730 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
657 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
731 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
658 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
732 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
659 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
733 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
660 }
734 }
661
735
662 to_test.each do |attrs, expected|
736 to_test.each do |attrs, expected|
663 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
737 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
664
738
665 assert user.valid?, user.errors.full_messages.to_s
739 assert user.valid?, user.errors.full_messages.to_s
666 assert_equal attrs.first, user.mail
740 assert_equal attrs.first, user.mail
667 assert_equal expected[0], user.login
741 assert_equal expected[0], user.login
668 assert_equal expected[1], user.firstname
742 assert_equal expected[1], user.firstname
669 assert_equal expected[2], user.lastname
743 assert_equal expected[2], user.lastname
670 end
744 end
671 end
745 end
672
746
673 def test_new_user_from_attributes_should_respect_minimum_password_length
747 def test_new_user_from_attributes_should_respect_minimum_password_length
674 with_settings :password_min_length => 15 do
748 with_settings :password_min_length => 15 do
675 user = MailHandler.new_user_from_attributes('jsmith@example.net')
749 user = MailHandler.new_user_from_attributes('jsmith@example.net')
676 assert user.valid?
750 assert user.valid?
677 assert user.password.length >= 15
751 assert user.password.length >= 15
678 end
752 end
679 end
753 end
680
754
681 def test_new_user_from_attributes_should_use_default_login_if_invalid
755 def test_new_user_from_attributes_should_use_default_login_if_invalid
682 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
756 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
683 assert user.valid?
757 assert user.valid?
684 assert user.login =~ /^user[a-f0-9]+$/
758 assert user.login =~ /^user[a-f0-9]+$/
685 assert_equal 'foo+bar@example.net', user.mail
759 assert_equal 'foo+bar@example.net', user.mail
686 end
760 end
687
761
688 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
762 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
689 assert_difference 'User.count' do
763 assert_difference 'User.count' do
690 issue = submit_email(
764 issue = submit_email(
691 'fullname_of_sender_as_utf8_encoded.eml',
765 'fullname_of_sender_as_utf8_encoded.eml',
692 :issue => {:project => 'ecookbook'},
766 :issue => {:project => 'ecookbook'},
693 :unknown_user => 'create'
767 :unknown_user => 'create'
694 )
768 )
695 end
769 end
696
770
697 user = User.first(:order => 'id DESC')
771 user = User.first(:order => 'id DESC')
698 assert_equal "foo@example.org", user.mail
772 assert_equal "foo@example.org", user.mail
699 str1 = "\xc3\x84\xc3\xa4"
773 str1 = "\xc3\x84\xc3\xa4"
700 str2 = "\xc3\x96\xc3\xb6"
774 str2 = "\xc3\x96\xc3\xb6"
701 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
775 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
702 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
776 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
703 assert_equal str1, user.firstname
777 assert_equal str1, user.firstname
704 assert_equal str2, user.lastname
778 assert_equal str2, user.lastname
705 end
779 end
706
780
707 private
781 private
708
782
709 def submit_email(filename, options={})
783 def submit_email(filename, options={})
710 raw = IO.read(File.join(FIXTURES_PATH, filename))
784 raw = IO.read(File.join(FIXTURES_PATH, filename))
711 yield raw if block_given?
785 yield raw if block_given?
712 MailHandler.receive(raw, options)
786 MailHandler.receive(raw, options)
713 end
787 end
714
788
715 def assert_issue_created(issue)
789 def assert_issue_created(issue)
716 assert issue.is_a?(Issue)
790 assert issue.is_a?(Issue)
717 assert !issue.new_record?
791 assert !issue.new_record?
718 issue.reload
792 issue.reload
719 end
793 end
720 end
794 end
General Comments 0
You need to be logged in to leave comments. Login now