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