##// END OF EJS Templates
Merged r6324 and r6325 from trunk (#8651)....
Jean-Philippe Lang -
r7646:03b2162e6c49
parent child
Show More
@@ -0,0 +1,240
1 From JSmith@somenet.foo Mon Jun 27 06:55:56 2011
2 Return-Path: <JSmith@somenet.foo>
3 X-Original-To: redmine@somenet.foo
4 Delivered-To: redmine@somenet.foo
5 From: John Smith <JSmith@somenet.foo>
6 Mime-Version: 1.0 (Apple Message framework v1084)
7 Content-Type: multipart/alternative; boundary=Apple-Mail-3-163265085
8 Subject: Test attaching images to tickets by HTML mail
9 Date: Mon, 27 Jun 2011 16:55:46 +0300
10 To: redmine@somenet.foo
11 Message-Id: <7ABE3636-07E8-47C9-90A1-FCB1AA894DA1@somenet.foo>
12 X-Mailer: Apple Mail (2.1084)
13
14
15 --Apple-Mail-3-163265085
16 Content-Transfer-Encoding: quoted-printable
17 Content-Type: text/plain;
18 charset=us-ascii
19
20 Yet another test!
21
22
23 --Apple-Mail-3-163265085
24 Content-Type: multipart/related;
25 type="text/html";
26 boundary=Apple-Mail-4-163265085
27
28
29 --Apple-Mail-4-163265085
30 Content-Transfer-Encoding: quoted-printable
31 Content-Type: text/html;
32 charset=us-ascii
33
34 <html><head></head><body style=3D"word-wrap: break-word; =
35 -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; ">
36 <br></body></html>=
37
38 --Apple-Mail-4-163265085
39 Content-Transfer-Encoding: base64
40 Content-Disposition: inline;
41 filename=paella.jpg
42 Content-Type: image/jpg;
43 x-unix-mode=0644;
44 name="paella.jpg"
45 Content-Id: <1207F0B5-9F9D-4AB4-B547-AF9033E82111>
46
47 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
236 cbis+/WpUqUcMZKdF44n/9k=
237
238 --Apple-Mail-4-163265085--
239
240 --Apple-Mail-3-163265085--
@@ -1,366 +1,366
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user
25 attr_reader :email, :user
26
26
27 def self.receive(email, options={})
27 def self.receive(email, options={})
28 @@handler_options = options.dup
28 @@handler_options = options.dup
29
29
30 @@handler_options[:issue] ||= {}
30 @@handler_options[:issue] ||= {}
31
31
32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] ||= []
33 @@handler_options[:allow_override] ||= []
34 # Project needs to be overridable if not specified
34 # Project needs to be overridable if not specified
35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
36 # Status overridable by default
36 # Status overridable by default
37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
38
38
39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
40 super email
40 super email
41 end
41 end
42
42
43 # Processes incoming emails
43 # Processes incoming emails
44 # Returns the created object (eg. an issue, a message) or false
44 # Returns the created object (eg. an issue, a message) or false
45 def receive(email)
45 def receive(email)
46 @email = email
46 @email = email
47 sender_email = email.from.to_a.first.to_s.strip
47 sender_email = email.from.to_a.first.to_s.strip
48 # Ignore emails received from the application emission address to avoid hell cycles
48 # Ignore emails received from the application emission address to avoid hell cycles
49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
51 return false
51 return false
52 end
52 end
53 @user = User.find_by_mail(sender_email) if sender_email.present?
53 @user = User.find_by_mail(sender_email) if sender_email.present?
54 if @user && !@user.active?
54 if @user && !@user.active?
55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
56 return false
56 return false
57 end
57 end
58 if @user.nil?
58 if @user.nil?
59 # Email was submitted by an unknown user
59 # Email was submitted by an unknown user
60 case @@handler_options[:unknown_user]
60 case @@handler_options[:unknown_user]
61 when 'accept'
61 when 'accept'
62 @user = User.anonymous
62 @user = User.anonymous
63 when 'create'
63 when 'create'
64 @user = MailHandler.create_user_from_email(email)
64 @user = MailHandler.create_user_from_email(email)
65 if @user
65 if @user
66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
67 Mailer.deliver_account_information(@user, @user.password)
67 Mailer.deliver_account_information(@user, @user.password)
68 else
68 else
69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
70 return false
70 return false
71 end
71 end
72 else
72 else
73 # Default behaviour, emails from unknown users are ignored
73 # Default behaviour, emails from unknown users are ignored
74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
75 return false
75 return false
76 end
76 end
77 end
77 end
78 User.current = @user
78 User.current = @user
79 dispatch
79 dispatch
80 end
80 end
81
81
82 private
82 private
83
83
84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
87
87
88 def dispatch
88 def dispatch
89 headers = [email.in_reply_to, email.references].flatten.compact
89 headers = [email.in_reply_to, email.references].flatten.compact
90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
91 klass, object_id = $1, $2.to_i
91 klass, object_id = $1, $2.to_i
92 method_name = "receive_#{klass}_reply"
92 method_name = "receive_#{klass}_reply"
93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
94 send method_name, object_id
94 send method_name, object_id
95 else
95 else
96 # ignoring it
96 # ignoring it
97 end
97 end
98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
99 receive_issue_reply(m[1].to_i)
99 receive_issue_reply(m[1].to_i)
100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
101 receive_message_reply(m[1].to_i)
101 receive_message_reply(m[1].to_i)
102 else
102 else
103 dispatch_to_default
103 dispatch_to_default
104 end
104 end
105 rescue ActiveRecord::RecordInvalid => e
105 rescue ActiveRecord::RecordInvalid => e
106 # TODO: send a email to the user
106 # TODO: send a email to the user
107 logger.error e.message if logger
107 logger.error e.message if logger
108 false
108 false
109 rescue MissingInformation => e
109 rescue MissingInformation => e
110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
111 false
111 false
112 rescue UnauthorizedAction => e
112 rescue UnauthorizedAction => e
113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
114 false
114 false
115 end
115 end
116
116
117 def dispatch_to_default
117 def dispatch_to_default
118 receive_issue
118 receive_issue
119 end
119 end
120
120
121 # Creates a new issue
121 # Creates a new issue
122 def receive_issue
122 def receive_issue
123 project = target_project
123 project = target_project
124 # check permission
124 # check permission
125 unless @@handler_options[:no_permission_check]
125 unless @@handler_options[:no_permission_check]
126 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
126 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
127 end
127 end
128
128
129 issue = Issue.new(:author => user, :project => project)
129 issue = Issue.new(:author => user, :project => project)
130 issue.safe_attributes = issue_attributes_from_keywords(issue)
130 issue.safe_attributes = issue_attributes_from_keywords(issue)
131 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
131 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
132 issue.subject = email.subject.to_s.chomp[0,255]
132 issue.subject = email.subject.to_s.chomp[0,255]
133 if issue.subject.blank?
133 if issue.subject.blank?
134 issue.subject = '(no subject)'
134 issue.subject = '(no subject)'
135 end
135 end
136 issue.description = cleaned_up_text_body
136 issue.description = cleaned_up_text_body
137
137
138 # add To and Cc as watchers before saving so the watchers can reply to Redmine
138 # add To and Cc as watchers before saving so the watchers can reply to Redmine
139 add_watchers(issue)
139 add_watchers(issue)
140 issue.save!
140 issue.save!
141 add_attachments(issue)
141 add_attachments(issue)
142 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
142 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
143 issue
143 issue
144 end
144 end
145
145
146 # Adds a note to an existing issue
146 # Adds a note to an existing issue
147 def receive_issue_reply(issue_id)
147 def receive_issue_reply(issue_id)
148 issue = Issue.find_by_id(issue_id)
148 issue = Issue.find_by_id(issue_id)
149 return unless issue
149 return unless issue
150 # check permission
150 # check permission
151 unless @@handler_options[:no_permission_check]
151 unless @@handler_options[:no_permission_check]
152 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
152 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
153 end
153 end
154
154
155 # ignore CLI-supplied defaults for new issues
155 # ignore CLI-supplied defaults for new issues
156 @@handler_options[:issue].clear
156 @@handler_options[:issue].clear
157
157
158 journal = issue.init_journal(user)
158 journal = issue.init_journal(user)
159 issue.safe_attributes = issue_attributes_from_keywords(issue)
159 issue.safe_attributes = issue_attributes_from_keywords(issue)
160 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
160 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
161 journal.notes = cleaned_up_text_body
161 journal.notes = cleaned_up_text_body
162 add_attachments(issue)
162 add_attachments(issue)
163 issue.save!
163 issue.save!
164 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
164 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
165 journal
165 journal
166 end
166 end
167
167
168 # Reply will be added to the issue
168 # Reply will be added to the issue
169 def receive_journal_reply(journal_id)
169 def receive_journal_reply(journal_id)
170 journal = Journal.find_by_id(journal_id)
170 journal = Journal.find_by_id(journal_id)
171 if journal && journal.journalized_type == 'Issue'
171 if journal && journal.journalized_type == 'Issue'
172 receive_issue_reply(journal.journalized_id)
172 receive_issue_reply(journal.journalized_id)
173 end
173 end
174 end
174 end
175
175
176 # Receives a reply to a forum message
176 # Receives a reply to a forum message
177 def receive_message_reply(message_id)
177 def receive_message_reply(message_id)
178 message = Message.find_by_id(message_id)
178 message = Message.find_by_id(message_id)
179 if message
179 if message
180 message = message.root
180 message = message.root
181
181
182 unless @@handler_options[:no_permission_check]
182 unless @@handler_options[:no_permission_check]
183 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
183 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
184 end
184 end
185
185
186 if !message.locked?
186 if !message.locked?
187 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
187 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
188 :content => cleaned_up_text_body)
188 :content => cleaned_up_text_body)
189 reply.author = user
189 reply.author = user
190 reply.board = message.board
190 reply.board = message.board
191 message.children << reply
191 message.children << reply
192 add_attachments(reply)
192 add_attachments(reply)
193 reply
193 reply
194 else
194 else
195 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
195 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
196 end
196 end
197 end
197 end
198 end
198 end
199
199
200 def add_attachments(obj)
200 def add_attachments(obj)
201 if email.has_attachments?
201 if email.attachments && email.attachments.any?
202 email.attachments.each do |attachment|
202 email.attachments.each do |attachment|
203 Attachment.create(:container => obj,
203 Attachment.create(:container => obj,
204 :file => attachment,
204 :file => attachment,
205 :author => user,
205 :author => user,
206 :content_type => attachment.content_type)
206 :content_type => attachment.content_type)
207 end
207 end
208 end
208 end
209 end
209 end
210
210
211 # Adds To and Cc as watchers of the given object if the sender has the
211 # Adds To and Cc as watchers of the given object if the sender has the
212 # appropriate permission
212 # appropriate permission
213 def add_watchers(obj)
213 def add_watchers(obj)
214 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
214 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
215 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
215 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
216 unless addresses.empty?
216 unless addresses.empty?
217 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
217 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
218 watchers.each {|w| obj.add_watcher(w)}
218 watchers.each {|w| obj.add_watcher(w)}
219 end
219 end
220 end
220 end
221 end
221 end
222
222
223 def get_keyword(attr, options={})
223 def get_keyword(attr, options={})
224 @keywords ||= {}
224 @keywords ||= {}
225 if @keywords.has_key?(attr)
225 if @keywords.has_key?(attr)
226 @keywords[attr]
226 @keywords[attr]
227 else
227 else
228 @keywords[attr] = begin
228 @keywords[attr] = begin
229 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
229 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
230 v
230 v
231 elsif !@@handler_options[:issue][attr].blank?
231 elsif !@@handler_options[:issue][attr].blank?
232 @@handler_options[:issue][attr]
232 @@handler_options[:issue][attr]
233 end
233 end
234 end
234 end
235 end
235 end
236 end
236 end
237
237
238 # Destructively extracts the value for +attr+ in +text+
238 # Destructively extracts the value for +attr+ in +text+
239 # Returns nil if no matching keyword found
239 # Returns nil if no matching keyword found
240 def extract_keyword!(text, attr, format=nil)
240 def extract_keyword!(text, attr, format=nil)
241 keys = [attr.to_s.humanize]
241 keys = [attr.to_s.humanize]
242 if attr.is_a?(Symbol)
242 if attr.is_a?(Symbol)
243 keys << l("field_#{attr}", :default => '', :locale => user.language) if user && user.language.present?
243 keys << l("field_#{attr}", :default => '', :locale => user.language) if user && user.language.present?
244 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present?
244 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present?
245 end
245 end
246 keys.reject! {|k| k.blank?}
246 keys.reject! {|k| k.blank?}
247 keys.collect! {|k| Regexp.escape(k)}
247 keys.collect! {|k| Regexp.escape(k)}
248 format ||= '.+'
248 format ||= '.+'
249 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
249 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
250 $2 && $2.strip
250 $2 && $2.strip
251 end
251 end
252
252
253 def target_project
253 def target_project
254 # TODO: other ways to specify project:
254 # TODO: other ways to specify project:
255 # * parse the email To field
255 # * parse the email To field
256 # * specific project (eg. Setting.mail_handler_target_project)
256 # * specific project (eg. Setting.mail_handler_target_project)
257 target = Project.find_by_identifier(get_keyword(:project))
257 target = Project.find_by_identifier(get_keyword(:project))
258 raise MissingInformation.new('Unable to determine target project') if target.nil?
258 raise MissingInformation.new('Unable to determine target project') if target.nil?
259 target
259 target
260 end
260 end
261
261
262 # Returns a Hash of issue attributes extracted from keywords in the email body
262 # Returns a Hash of issue attributes extracted from keywords in the email body
263 def issue_attributes_from_keywords(issue)
263 def issue_attributes_from_keywords(issue)
264 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
264 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
265 assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
265 assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
266
266
267 attrs = {
267 attrs = {
268 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
268 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
269 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
269 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
270 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
270 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
271 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
271 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
272 'assigned_to_id' => assigned_to.try(:id),
272 'assigned_to_id' => assigned_to.try(:id),
273 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id),
273 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id),
274 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
274 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
275 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
275 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
276 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
276 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
277 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
277 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
278 }.delete_if {|k, v| v.blank? }
278 }.delete_if {|k, v| v.blank? }
279
279
280 if issue.new_record? && attrs['tracker_id'].nil?
280 if issue.new_record? && attrs['tracker_id'].nil?
281 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
281 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
282 end
282 end
283
283
284 attrs
284 attrs
285 end
285 end
286
286
287 # Returns a Hash of issue custom field values extracted from keywords in the email body
287 # Returns a Hash of issue custom field values extracted from keywords in the email body
288 def custom_field_values_from_keywords(customized)
288 def custom_field_values_from_keywords(customized)
289 customized.custom_field_values.inject({}) do |h, v|
289 customized.custom_field_values.inject({}) do |h, v|
290 if value = get_keyword(v.custom_field.name, :override => true)
290 if value = get_keyword(v.custom_field.name, :override => true)
291 h[v.custom_field.id.to_s] = value
291 h[v.custom_field.id.to_s] = value
292 end
292 end
293 h
293 h
294 end
294 end
295 end
295 end
296
296
297 # Returns the text/plain part of the email
297 # Returns the text/plain part of the email
298 # If not found (eg. HTML-only email), returns the body with tags removed
298 # If not found (eg. HTML-only email), returns the body with tags removed
299 def plain_text_body
299 def plain_text_body
300 return @plain_text_body unless @plain_text_body.nil?
300 return @plain_text_body unless @plain_text_body.nil?
301 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
301 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
302 if parts.empty?
302 if parts.empty?
303 parts << @email
303 parts << @email
304 end
304 end
305 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
305 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
306 if plain_text_part.nil?
306 if plain_text_part.nil?
307 # no text/plain part found, assuming html-only email
307 # no text/plain part found, assuming html-only email
308 # strip html tags and remove doctype directive
308 # strip html tags and remove doctype directive
309 @plain_text_body = strip_tags(@email.body.to_s)
309 @plain_text_body = strip_tags(@email.body.to_s)
310 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
310 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
311 else
311 else
312 @plain_text_body = plain_text_part.body.to_s
312 @plain_text_body = plain_text_part.body.to_s
313 end
313 end
314 @plain_text_body.strip!
314 @plain_text_body.strip!
315 @plain_text_body
315 @plain_text_body
316 end
316 end
317
317
318 def cleaned_up_text_body
318 def cleaned_up_text_body
319 cleanup_body(plain_text_body)
319 cleanup_body(plain_text_body)
320 end
320 end
321
321
322 def self.full_sanitizer
322 def self.full_sanitizer
323 @full_sanitizer ||= HTML::FullSanitizer.new
323 @full_sanitizer ||= HTML::FullSanitizer.new
324 end
324 end
325
325
326 # Creates a user account for the +email+ sender
326 # Creates a user account for the +email+ sender
327 def self.create_user_from_email(email)
327 def self.create_user_from_email(email)
328 addr = email.from_addrs.to_a.first
328 addr = email.from_addrs.to_a.first
329 if addr && !addr.spec.blank?
329 if addr && !addr.spec.blank?
330 user = User.new
330 user = User.new
331 user.mail = addr.spec
331 user.mail = addr.spec
332
332
333 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
333 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
334 user.firstname = names.shift
334 user.firstname = names.shift
335 user.lastname = names.join(' ')
335 user.lastname = names.join(' ')
336 user.lastname = '-' if user.lastname.blank?
336 user.lastname = '-' if user.lastname.blank?
337
337
338 user.login = user.mail
338 user.login = user.mail
339 user.password = ActiveSupport::SecureRandom.hex(5)
339 user.password = ActiveSupport::SecureRandom.hex(5)
340 user.language = Setting.default_language
340 user.language = Setting.default_language
341 user.save ? user : nil
341 user.save ? user : nil
342 end
342 end
343 end
343 end
344
344
345 private
345 private
346
346
347 # Removes the email body of text after the truncation configurations.
347 # Removes the email body of text after the truncation configurations.
348 def cleanup_body(body)
348 def cleanup_body(body)
349 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
349 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
350 unless delimiters.empty?
350 unless delimiters.empty?
351 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
351 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
352 body = body.gsub(regex, '')
352 body = body.gsub(regex, '')
353 end
353 end
354 body.strip
354 body.strip
355 end
355 end
356
356
357 def find_user_from_keyword(keyword)
357 def find_user_from_keyword(keyword)
358 user ||= User.find_by_mail(keyword)
358 user ||= User.find_by_mail(keyword)
359 user ||= User.find_by_login(keyword)
359 user ||= User.find_by_login(keyword)
360 if user.nil? && keyword.match(/ /)
360 if user.nil? && keyword.match(/ /)
361 firstname, lastname = *(keyword.split) # "First Last Throwaway"
361 firstname, lastname = *(keyword.split) # "First Last Throwaway"
362 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
362 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
363 end
363 end
364 user
364 user
365 end
365 end
366 end
366 end
@@ -1,457 +1,467
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 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,
23 fixtures :users, :projects,
24 :enabled_modules,
24 :enabled_modules,
25 :roles,
25 :roles,
26 :members,
26 :members,
27 :member_roles,
27 :member_roles,
28 :users,
28 :users,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :workflows,
31 :workflows,
32 :trackers,
32 :trackers,
33 :projects_trackers,
33 :projects_trackers,
34 :versions,
34 :versions,
35 :enumerations,
35 :enumerations,
36 :issue_categories,
36 :issue_categories,
37 :custom_fields,
37 :custom_fields,
38 :custom_fields_trackers,
38 :custom_fields_trackers,
39 :custom_fields_projects,
39 :custom_fields_projects,
40 :boards,
40 :boards,
41 :messages
41 :messages
42
42
43 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
43 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
44
44
45 def setup
45 def setup
46 ActionMailer::Base.deliveries.clear
46 ActionMailer::Base.deliveries.clear
47 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
47 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
48 end
48 end
49
49
50 def test_add_issue
50 def test_add_issue
51 ActionMailer::Base.deliveries.clear
51 ActionMailer::Base.deliveries.clear
52 # This email contains: 'Project: onlinestore'
52 # This email contains: 'Project: onlinestore'
53 issue = submit_email('ticket_on_given_project.eml')
53 issue = submit_email('ticket_on_given_project.eml')
54 assert issue.is_a?(Issue)
54 assert issue.is_a?(Issue)
55 assert !issue.new_record?
55 assert !issue.new_record?
56 issue.reload
56 issue.reload
57 assert_equal Project.find(2), issue.project
57 assert_equal Project.find(2), issue.project
58 assert_equal issue.project.trackers.first, issue.tracker
58 assert_equal issue.project.trackers.first, issue.tracker
59 assert_equal 'New ticket on a given project', issue.subject
59 assert_equal 'New ticket on a given project', issue.subject
60 assert_equal User.find_by_login('jsmith'), issue.author
60 assert_equal User.find_by_login('jsmith'), issue.author
61 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
61 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
62 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
62 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
63 assert_equal '2010-01-01', issue.start_date.to_s
63 assert_equal '2010-01-01', issue.start_date.to_s
64 assert_equal '2010-12-31', issue.due_date.to_s
64 assert_equal '2010-12-31', issue.due_date.to_s
65 assert_equal User.find_by_login('jsmith'), issue.assigned_to
65 assert_equal User.find_by_login('jsmith'), issue.assigned_to
66 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
66 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
67 assert_equal 2.5, issue.estimated_hours
67 assert_equal 2.5, issue.estimated_hours
68 assert_equal 30, issue.done_ratio
68 assert_equal 30, issue.done_ratio
69 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
69 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
70 # keywords should be removed from the email body
70 # keywords should be removed from the email body
71 assert !issue.description.match(/^Project:/i)
71 assert !issue.description.match(/^Project:/i)
72 assert !issue.description.match(/^Status:/i)
72 assert !issue.description.match(/^Status:/i)
73 assert !issue.description.match(/^Start Date:/i)
73 assert !issue.description.match(/^Start Date:/i)
74 # Email notification should be sent
74 # Email notification should be sent
75 mail = ActionMailer::Base.deliveries.last
75 mail = ActionMailer::Base.deliveries.last
76 assert_not_nil mail
76 assert_not_nil mail
77 assert mail.subject.include?('New ticket on a given project')
77 assert mail.subject.include?('New ticket on a given project')
78 end
78 end
79
79
80 def test_add_issue_with_default_tracker
80 def test_add_issue_with_default_tracker
81 # This email contains: 'Project: onlinestore'
81 # This email contains: 'Project: onlinestore'
82 issue = submit_email('ticket_on_given_project.eml', :issue => {:tracker => 'Support request'})
82 issue = submit_email('ticket_on_given_project.eml', :issue => {:tracker => 'Support request'})
83 assert issue.is_a?(Issue)
83 assert issue.is_a?(Issue)
84 assert !issue.new_record?
84 assert !issue.new_record?
85 issue.reload
85 issue.reload
86 assert_equal 'Support request', issue.tracker.name
86 assert_equal 'Support request', issue.tracker.name
87 end
87 end
88
88
89 def test_add_issue_with_status
89 def test_add_issue_with_status
90 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
90 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
91 issue = submit_email('ticket_on_given_project.eml')
91 issue = submit_email('ticket_on_given_project.eml')
92 assert issue.is_a?(Issue)
92 assert issue.is_a?(Issue)
93 assert !issue.new_record?
93 assert !issue.new_record?
94 issue.reload
94 issue.reload
95 assert_equal Project.find(2), issue.project
95 assert_equal Project.find(2), issue.project
96 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
96 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
97 end
97 end
98
98
99 def test_add_issue_with_attributes_override
99 def test_add_issue_with_attributes_override
100 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
100 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
101 assert issue.is_a?(Issue)
101 assert issue.is_a?(Issue)
102 assert !issue.new_record?
102 assert !issue.new_record?
103 issue.reload
103 issue.reload
104 assert_equal 'New ticket on a given project', issue.subject
104 assert_equal 'New ticket on a given project', issue.subject
105 assert_equal User.find_by_login('jsmith'), issue.author
105 assert_equal User.find_by_login('jsmith'), issue.author
106 assert_equal Project.find(2), issue.project
106 assert_equal Project.find(2), issue.project
107 assert_equal 'Feature request', issue.tracker.to_s
107 assert_equal 'Feature request', issue.tracker.to_s
108 assert_equal 'Stock management', issue.category.to_s
108 assert_equal 'Stock management', issue.category.to_s
109 assert_equal 'Urgent', issue.priority.to_s
109 assert_equal 'Urgent', issue.priority.to_s
110 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
110 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
111 end
111 end
112
112
113 def test_add_issue_with_partial_attributes_override
113 def test_add_issue_with_partial_attributes_override
114 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
114 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
115 assert issue.is_a?(Issue)
115 assert issue.is_a?(Issue)
116 assert !issue.new_record?
116 assert !issue.new_record?
117 issue.reload
117 issue.reload
118 assert_equal 'New ticket on a given project', issue.subject
118 assert_equal 'New ticket on a given project', issue.subject
119 assert_equal User.find_by_login('jsmith'), issue.author
119 assert_equal User.find_by_login('jsmith'), issue.author
120 assert_equal Project.find(2), issue.project
120 assert_equal Project.find(2), issue.project
121 assert_equal 'Feature request', issue.tracker.to_s
121 assert_equal 'Feature request', issue.tracker.to_s
122 assert_nil issue.category
122 assert_nil issue.category
123 assert_equal 'High', issue.priority.to_s
123 assert_equal 'High', issue.priority.to_s
124 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
124 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
125 end
125 end
126
126
127 def test_add_issue_with_spaces_between_attribute_and_separator
127 def test_add_issue_with_spaces_between_attribute_and_separator
128 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
128 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
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_equal 'Stock management', issue.category.to_s
136 assert_equal 'Stock management', issue.category.to_s
137 assert_equal 'Urgent', issue.priority.to_s
137 assert_equal 'Urgent', 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_attachment_to_specific_project
141 def test_add_issue_with_attachment_to_specific_project
142 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
142 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
143 assert issue.is_a?(Issue)
143 assert issue.is_a?(Issue)
144 assert !issue.new_record?
144 assert !issue.new_record?
145 issue.reload
145 issue.reload
146 assert_equal 'Ticket created by email with attachment', issue.subject
146 assert_equal 'Ticket created by email with attachment', issue.subject
147 assert_equal User.find_by_login('jsmith'), issue.author
147 assert_equal User.find_by_login('jsmith'), issue.author
148 assert_equal Project.find(2), issue.project
148 assert_equal Project.find(2), issue.project
149 assert_equal 'This is a new ticket with attachments', issue.description
149 assert_equal 'This is a new ticket with attachments', issue.description
150 # Attachment properties
150 # Attachment properties
151 assert_equal 1, issue.attachments.size
151 assert_equal 1, issue.attachments.size
152 assert_equal 'Paella.jpg', issue.attachments.first.filename
152 assert_equal 'Paella.jpg', issue.attachments.first.filename
153 assert_equal 'image/jpeg', issue.attachments.first.content_type
153 assert_equal 'image/jpeg', issue.attachments.first.content_type
154 assert_equal 10790, issue.attachments.first.filesize
154 assert_equal 10790, issue.attachments.first.filesize
155 end
155 end
156
156
157 def test_add_issue_with_custom_fields
157 def test_add_issue_with_custom_fields
158 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
158 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
159 assert issue.is_a?(Issue)
159 assert issue.is_a?(Issue)
160 assert !issue.new_record?
160 assert !issue.new_record?
161 issue.reload
161 issue.reload
162 assert_equal 'New ticket with custom field values', issue.subject
162 assert_equal 'New ticket with custom field values', issue.subject
163 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
163 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
164 assert !issue.description.match(/^searchable field:/i)
164 assert !issue.description.match(/^searchable field:/i)
165 end
165 end
166
166
167 def test_add_issue_with_cc
167 def test_add_issue_with_cc
168 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
168 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
169 assert issue.is_a?(Issue)
169 assert issue.is_a?(Issue)
170 assert !issue.new_record?
170 assert !issue.new_record?
171 issue.reload
171 issue.reload
172 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
172 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
173 assert_equal 1, issue.watcher_user_ids.size
173 assert_equal 1, issue.watcher_user_ids.size
174 end
174 end
175
175
176 def test_add_issue_by_unknown_user
176 def test_add_issue_by_unknown_user
177 assert_no_difference 'User.count' do
177 assert_no_difference 'User.count' do
178 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
178 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
179 end
179 end
180 end
180 end
181
181
182 def test_add_issue_by_anonymous_user
182 def test_add_issue_by_anonymous_user
183 Role.anonymous.add_permission!(:add_issues)
183 Role.anonymous.add_permission!(:add_issues)
184 assert_no_difference 'User.count' do
184 assert_no_difference 'User.count' do
185 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
185 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
186 assert issue.is_a?(Issue)
186 assert issue.is_a?(Issue)
187 assert issue.author.anonymous?
187 assert issue.author.anonymous?
188 end
188 end
189 end
189 end
190
190
191 def test_add_issue_by_anonymous_user_with_no_from_address
191 def test_add_issue_by_anonymous_user_with_no_from_address
192 Role.anonymous.add_permission!(:add_issues)
192 Role.anonymous.add_permission!(:add_issues)
193 assert_no_difference 'User.count' do
193 assert_no_difference 'User.count' do
194 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
194 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
195 assert issue.is_a?(Issue)
195 assert issue.is_a?(Issue)
196 assert issue.author.anonymous?
196 assert issue.author.anonymous?
197 end
197 end
198 end
198 end
199
199
200 def test_add_issue_by_anonymous_user_on_private_project
200 def test_add_issue_by_anonymous_user_on_private_project
201 Role.anonymous.add_permission!(:add_issues)
201 Role.anonymous.add_permission!(:add_issues)
202 assert_no_difference 'User.count' do
202 assert_no_difference 'User.count' do
203 assert_no_difference 'Issue.count' do
203 assert_no_difference 'Issue.count' do
204 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
204 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
205 end
205 end
206 end
206 end
207 end
207 end
208
208
209 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
209 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
210 assert_no_difference 'User.count' do
210 assert_no_difference 'User.count' do
211 assert_difference 'Issue.count' do
211 assert_difference 'Issue.count' do
212 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
212 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
213 assert issue.is_a?(Issue)
213 assert issue.is_a?(Issue)
214 assert issue.author.anonymous?
214 assert issue.author.anonymous?
215 assert !issue.project.is_public?
215 assert !issue.project.is_public?
216 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
216 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
217 end
217 end
218 end
218 end
219 end
219 end
220
220
221 def test_add_issue_by_created_user
221 def test_add_issue_by_created_user
222 Setting.default_language = 'en'
222 Setting.default_language = 'en'
223 assert_difference 'User.count' do
223 assert_difference 'User.count' do
224 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
224 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
225 assert issue.is_a?(Issue)
225 assert issue.is_a?(Issue)
226 assert issue.author.active?
226 assert issue.author.active?
227 assert_equal 'john.doe@somenet.foo', issue.author.mail
227 assert_equal 'john.doe@somenet.foo', issue.author.mail
228 assert_equal 'John', issue.author.firstname
228 assert_equal 'John', issue.author.firstname
229 assert_equal 'Doe', issue.author.lastname
229 assert_equal 'Doe', issue.author.lastname
230
230
231 # account information
231 # account information
232 email = ActionMailer::Base.deliveries.first
232 email = ActionMailer::Base.deliveries.first
233 assert_not_nil email
233 assert_not_nil email
234 assert email.subject.include?('account activation')
234 assert email.subject.include?('account activation')
235 login = email.body.match(/\* Login: (.*)$/)[1]
235 login = email.body.match(/\* Login: (.*)$/)[1]
236 password = email.body.match(/\* Password: (.*)$/)[1]
236 password = email.body.match(/\* Password: (.*)$/)[1]
237 assert_equal issue.author, User.try_to_login(login, password)
237 assert_equal issue.author, User.try_to_login(login, password)
238 end
238 end
239 end
239 end
240
240
241 def test_add_issue_without_from_header
241 def test_add_issue_without_from_header
242 Role.anonymous.add_permission!(:add_issues)
242 Role.anonymous.add_permission!(:add_issues)
243 assert_equal false, submit_email('ticket_without_from_header.eml')
243 assert_equal false, submit_email('ticket_without_from_header.eml')
244 end
244 end
245
245
246 def test_add_issue_with_invalid_attributes
246 def test_add_issue_with_invalid_attributes
247 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
247 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
248 assert issue.is_a?(Issue)
248 assert issue.is_a?(Issue)
249 assert !issue.new_record?
249 assert !issue.new_record?
250 issue.reload
250 issue.reload
251 assert_nil issue.assigned_to
251 assert_nil issue.assigned_to
252 assert_nil issue.start_date
252 assert_nil issue.start_date
253 assert_nil issue.due_date
253 assert_nil issue.due_date
254 assert_equal 0, issue.done_ratio
254 assert_equal 0, issue.done_ratio
255 assert_equal 'Normal', issue.priority.to_s
255 assert_equal 'Normal', issue.priority.to_s
256 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
256 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
257 end
257 end
258
258
259 def test_add_issue_with_localized_attributes
259 def test_add_issue_with_localized_attributes
260 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
260 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
261 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
261 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
262 assert issue.is_a?(Issue)
262 assert issue.is_a?(Issue)
263 assert !issue.new_record?
263 assert !issue.new_record?
264 issue.reload
264 issue.reload
265 assert_equal 'New ticket on a given project', issue.subject
265 assert_equal 'New ticket on a given project', issue.subject
266 assert_equal User.find_by_login('jsmith'), issue.author
266 assert_equal User.find_by_login('jsmith'), issue.author
267 assert_equal Project.find(2), issue.project
267 assert_equal Project.find(2), issue.project
268 assert_equal 'Feature request', issue.tracker.to_s
268 assert_equal 'Feature request', issue.tracker.to_s
269 assert_equal 'Stock management', issue.category.to_s
269 assert_equal 'Stock management', issue.category.to_s
270 assert_equal 'Urgent', issue.priority.to_s
270 assert_equal 'Urgent', issue.priority.to_s
271 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
271 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
272 end
272 end
273
273
274 def test_add_issue_with_japanese_keywords
274 def test_add_issue_with_japanese_keywords
275 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
275 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
276 Project.find(1).trackers << tracker
276 Project.find(1).trackers << tracker
277 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
277 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
278 assert_kind_of Issue, issue
278 assert_kind_of Issue, issue
279 assert_equal tracker, issue.tracker
279 assert_equal tracker, issue.tracker
280 end
280 end
281
281
282 def test_add_issue_from_apple_mail
283 issue = submit_email('apple_mail_with_attachment.eml', :issue => {:project => 'ecookbook'})
284 assert_kind_of Issue, issue
285 assert_equal 1, issue.attachments.size
286
287 attachment = issue.attachments.first
288 assert_equal 'paella.jpg', attachment.filename
289 assert_equal 10790, attachment.filesize
290 end
291
282 def test_should_ignore_emails_from_emission_address
292 def test_should_ignore_emails_from_emission_address
283 Role.anonymous.add_permission!(:add_issues)
293 Role.anonymous.add_permission!(:add_issues)
284 assert_no_difference 'User.count' do
294 assert_no_difference 'User.count' do
285 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
295 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
286 end
296 end
287 end
297 end
288
298
289 def test_add_issue_should_send_email_notification
299 def test_add_issue_should_send_email_notification
290 Setting.notified_events = ['issue_added']
300 Setting.notified_events = ['issue_added']
291 ActionMailer::Base.deliveries.clear
301 ActionMailer::Base.deliveries.clear
292 # This email contains: 'Project: onlinestore'
302 # This email contains: 'Project: onlinestore'
293 issue = submit_email('ticket_on_given_project.eml')
303 issue = submit_email('ticket_on_given_project.eml')
294 assert issue.is_a?(Issue)
304 assert issue.is_a?(Issue)
295 assert_equal 1, ActionMailer::Base.deliveries.size
305 assert_equal 1, ActionMailer::Base.deliveries.size
296 end
306 end
297
307
298 def test_add_issue_note
308 def test_add_issue_note
299 journal = submit_email('ticket_reply.eml')
309 journal = submit_email('ticket_reply.eml')
300 assert journal.is_a?(Journal)
310 assert journal.is_a?(Journal)
301 assert_equal User.find_by_login('jsmith'), journal.user
311 assert_equal User.find_by_login('jsmith'), journal.user
302 assert_equal Issue.find(2), journal.journalized
312 assert_equal Issue.find(2), journal.journalized
303 assert_match /This is reply/, journal.notes
313 assert_match /This is reply/, journal.notes
304 assert_equal 'Feature request', journal.issue.tracker.name
314 assert_equal 'Feature request', journal.issue.tracker.name
305 end
315 end
306
316
307 def test_add_issue_note_with_attribute_changes
317 def test_add_issue_note_with_attribute_changes
308 # This email contains: 'Status: Resolved'
318 # This email contains: 'Status: Resolved'
309 journal = submit_email('ticket_reply_with_status.eml')
319 journal = submit_email('ticket_reply_with_status.eml')
310 assert journal.is_a?(Journal)
320 assert journal.is_a?(Journal)
311 issue = Issue.find(journal.issue.id)
321 issue = Issue.find(journal.issue.id)
312 assert_equal User.find_by_login('jsmith'), journal.user
322 assert_equal User.find_by_login('jsmith'), journal.user
313 assert_equal Issue.find(2), journal.journalized
323 assert_equal Issue.find(2), journal.journalized
314 assert_match /This is reply/, journal.notes
324 assert_match /This is reply/, journal.notes
315 assert_equal 'Feature request', journal.issue.tracker.name
325 assert_equal 'Feature request', journal.issue.tracker.name
316 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
326 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
317 assert_equal '2010-01-01', issue.start_date.to_s
327 assert_equal '2010-01-01', issue.start_date.to_s
318 assert_equal '2010-12-31', issue.due_date.to_s
328 assert_equal '2010-12-31', issue.due_date.to_s
319 assert_equal User.find_by_login('jsmith'), issue.assigned_to
329 assert_equal User.find_by_login('jsmith'), issue.assigned_to
320 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
330 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
321 # keywords should be removed from the email body
331 # keywords should be removed from the email body
322 assert !journal.notes.match(/^Status:/i)
332 assert !journal.notes.match(/^Status:/i)
323 assert !journal.notes.match(/^Start Date:/i)
333 assert !journal.notes.match(/^Start Date:/i)
324 end
334 end
325
335
326 def test_add_issue_note_should_send_email_notification
336 def test_add_issue_note_should_send_email_notification
327 ActionMailer::Base.deliveries.clear
337 ActionMailer::Base.deliveries.clear
328 journal = submit_email('ticket_reply.eml')
338 journal = submit_email('ticket_reply.eml')
329 assert journal.is_a?(Journal)
339 assert journal.is_a?(Journal)
330 assert_equal 1, ActionMailer::Base.deliveries.size
340 assert_equal 1, ActionMailer::Base.deliveries.size
331 end
341 end
332
342
333 def test_add_issue_note_should_not_set_defaults
343 def test_add_issue_note_should_not_set_defaults
334 journal = submit_email('ticket_reply.eml', :issue => {:tracker => 'Support request', :priority => 'High'})
344 journal = submit_email('ticket_reply.eml', :issue => {:tracker => 'Support request', :priority => 'High'})
335 assert journal.is_a?(Journal)
345 assert journal.is_a?(Journal)
336 assert_match /This is reply/, journal.notes
346 assert_match /This is reply/, journal.notes
337 assert_equal 'Feature request', journal.issue.tracker.name
347 assert_equal 'Feature request', journal.issue.tracker.name
338 assert_equal 'Normal', journal.issue.priority.name
348 assert_equal 'Normal', journal.issue.priority.name
339 end
349 end
340
350
341 def test_reply_to_a_message
351 def test_reply_to_a_message
342 m = submit_email('message_reply.eml')
352 m = submit_email('message_reply.eml')
343 assert m.is_a?(Message)
353 assert m.is_a?(Message)
344 assert !m.new_record?
354 assert !m.new_record?
345 m.reload
355 m.reload
346 assert_equal 'Reply via email', m.subject
356 assert_equal 'Reply via email', m.subject
347 # The email replies to message #2 which is part of the thread of message #1
357 # The email replies to message #2 which is part of the thread of message #1
348 assert_equal Message.find(1), m.parent
358 assert_equal Message.find(1), m.parent
349 end
359 end
350
360
351 def test_reply_to_a_message_by_subject
361 def test_reply_to_a_message_by_subject
352 m = submit_email('message_reply_by_subject.eml')
362 m = submit_email('message_reply_by_subject.eml')
353 assert m.is_a?(Message)
363 assert m.is_a?(Message)
354 assert !m.new_record?
364 assert !m.new_record?
355 m.reload
365 m.reload
356 assert_equal 'Reply to the first post', m.subject
366 assert_equal 'Reply to the first post', m.subject
357 assert_equal Message.find(1), m.parent
367 assert_equal Message.find(1), m.parent
358 end
368 end
359
369
360 def test_should_strip_tags_of_html_only_emails
370 def test_should_strip_tags_of_html_only_emails
361 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
371 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
362 assert issue.is_a?(Issue)
372 assert issue.is_a?(Issue)
363 assert !issue.new_record?
373 assert !issue.new_record?
364 issue.reload
374 issue.reload
365 assert_equal 'HTML email', issue.subject
375 assert_equal 'HTML email', issue.subject
366 assert_equal 'This is a html-only email.', issue.description
376 assert_equal 'This is a html-only email.', issue.description
367 end
377 end
368
378
369 context "truncate emails based on the Setting" do
379 context "truncate emails based on the Setting" do
370 context "with no setting" do
380 context "with no setting" do
371 setup do
381 setup do
372 Setting.mail_handler_body_delimiters = ''
382 Setting.mail_handler_body_delimiters = ''
373 end
383 end
374
384
375 should "add the entire email into the issue" do
385 should "add the entire email into the issue" do
376 issue = submit_email('ticket_on_given_project.eml')
386 issue = submit_email('ticket_on_given_project.eml')
377 assert_issue_created(issue)
387 assert_issue_created(issue)
378 assert issue.description.include?('---')
388 assert issue.description.include?('---')
379 assert issue.description.include?('This paragraph is after the delimiter')
389 assert issue.description.include?('This paragraph is after the delimiter')
380 end
390 end
381 end
391 end
382
392
383 context "with a single string" do
393 context "with a single string" do
384 setup do
394 setup do
385 Setting.mail_handler_body_delimiters = '---'
395 Setting.mail_handler_body_delimiters = '---'
386 end
396 end
387 should "truncate the email at the delimiter for the issue" do
397 should "truncate the email at the delimiter for the issue" do
388 issue = submit_email('ticket_on_given_project.eml')
398 issue = submit_email('ticket_on_given_project.eml')
389 assert_issue_created(issue)
399 assert_issue_created(issue)
390 assert issue.description.include?('This paragraph is before delimiters')
400 assert issue.description.include?('This paragraph is before delimiters')
391 assert issue.description.include?('--- This line starts with a delimiter')
401 assert issue.description.include?('--- This line starts with a delimiter')
392 assert !issue.description.match(/^---$/)
402 assert !issue.description.match(/^---$/)
393 assert !issue.description.include?('This paragraph is after the delimiter')
403 assert !issue.description.include?('This paragraph is after the delimiter')
394 end
404 end
395 end
405 end
396
406
397 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
407 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
398 setup do
408 setup do
399 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
409 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
400 end
410 end
401 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
411 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
402 journal = submit_email('issue_update_with_quoted_reply_above.eml')
412 journal = submit_email('issue_update_with_quoted_reply_above.eml')
403 assert journal.is_a?(Journal)
413 assert journal.is_a?(Journal)
404 assert journal.notes.include?('An update to the issue by the sender.')
414 assert journal.notes.include?('An update to the issue by the sender.')
405 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
415 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
406 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
416 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
407 end
417 end
408 end
418 end
409
419
410 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
420 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
411 setup do
421 setup do
412 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
422 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
413 end
423 end
414 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
424 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
415 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
425 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
416 assert journal.is_a?(Journal)
426 assert journal.is_a?(Journal)
417 assert journal.notes.include?('An update to the issue by the sender.')
427 assert journal.notes.include?('An update to the issue by the sender.')
418 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
428 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
419 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
429 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
420 end
430 end
421 end
431 end
422
432
423 context "with multiple strings" do
433 context "with multiple strings" do
424 setup do
434 setup do
425 Setting.mail_handler_body_delimiters = "---\nBREAK"
435 Setting.mail_handler_body_delimiters = "---\nBREAK"
426 end
436 end
427 should "truncate the email at the first delimiter found (BREAK)" do
437 should "truncate the email at the first delimiter found (BREAK)" do
428 issue = submit_email('ticket_on_given_project.eml')
438 issue = submit_email('ticket_on_given_project.eml')
429 assert_issue_created(issue)
439 assert_issue_created(issue)
430 assert issue.description.include?('This paragraph is before delimiters')
440 assert issue.description.include?('This paragraph is before delimiters')
431 assert !issue.description.include?('BREAK')
441 assert !issue.description.include?('BREAK')
432 assert !issue.description.include?('This paragraph is between delimiters')
442 assert !issue.description.include?('This paragraph is between delimiters')
433 assert !issue.description.match(/^---$/)
443 assert !issue.description.match(/^---$/)
434 assert !issue.description.include?('This paragraph is after the delimiter')
444 assert !issue.description.include?('This paragraph is after the delimiter')
435 end
445 end
436 end
446 end
437 end
447 end
438
448
439 def test_email_with_long_subject_line
449 def test_email_with_long_subject_line
440 issue = submit_email('ticket_with_long_subject.eml')
450 issue = submit_email('ticket_with_long_subject.eml')
441 assert issue.is_a?(Issue)
451 assert issue.is_a?(Issue)
442 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]
452 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]
443 end
453 end
444
454
445 private
455 private
446
456
447 def submit_email(filename, options={})
457 def submit_email(filename, options={})
448 raw = IO.read(File.join(FIXTURES_PATH, filename))
458 raw = IO.read(File.join(FIXTURES_PATH, filename))
449 MailHandler.receive(raw, options)
459 MailHandler.receive(raw, options)
450 end
460 end
451
461
452 def assert_issue_created(issue)
462 def assert_issue_created(issue)
453 assert issue.is_a?(Issue)
463 assert issue.is_a?(Issue)
454 assert !issue.new_record?
464 assert !issue.new_record?
455 issue.reload
465 issue.reload
456 end
466 end
457 end
467 end
General Comments 0
You need to be logged in to leave comments. Login now