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