##// END OF EJS Templates
Adds a 'no_permission_check' option to the MailHandler....
Jean-Philippe Lang -
r3081:06ca18b04225
parent child
Show More
@@ -1,290 +1,302
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20
20
21 class UnauthorizedAction < StandardError; end
21 class UnauthorizedAction < StandardError; end
22 class MissingInformation < StandardError; end
22 class MissingInformation < StandardError; end
23
23
24 attr_reader :email, :user
24 attr_reader :email, :user
25
25
26 def self.receive(email, options={})
26 def self.receive(email, options={})
27 @@handler_options = options.dup
27 @@handler_options = options.dup
28
28
29 @@handler_options[:issue] ||= {}
29 @@handler_options[:issue] ||= {}
30
30
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 @@handler_options[:allow_override] ||= []
32 @@handler_options[:allow_override] ||= []
33 # Project needs to be overridable if not specified
33 # Project needs to be overridable if not specified
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 # Status overridable by default
35 # Status overridable by default
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37
38 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
37 super email
39 super email
38 end
40 end
39
41
40 # Processes incoming emails
42 # Processes incoming emails
41 # Returns the created object (eg. an issue, a message) or false
43 # Returns the created object (eg. an issue, a message) or false
42 def receive(email)
44 def receive(email)
43 @email = email
45 @email = email
44 sender_email = email.from.to_a.first.to_s.strip
46 sender_email = email.from.to_a.first.to_s.strip
45 # Ignore emails received from the application emission address to avoid hell cycles
47 # Ignore emails received from the application emission address to avoid hell cycles
46 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
48 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
47 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
49 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
48 return false
50 return false
49 end
51 end
50 @user = User.find_by_mail(sender_email)
52 @user = User.find_by_mail(sender_email)
51 if @user && !@user.active?
53 if @user && !@user.active?
52 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
54 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
53 return false
55 return false
54 end
56 end
55 if @user.nil?
57 if @user.nil?
56 # Email was submitted by an unknown user
58 # Email was submitted by an unknown user
57 case @@handler_options[:unknown_user]
59 case @@handler_options[:unknown_user]
58 when 'accept'
60 when 'accept'
59 @user = User.anonymous
61 @user = User.anonymous
60 when 'create'
62 when 'create'
61 @user = MailHandler.create_user_from_email(email)
63 @user = MailHandler.create_user_from_email(email)
62 if @user
64 if @user
63 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
65 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
64 Mailer.deliver_account_information(@user, @user.password)
66 Mailer.deliver_account_information(@user, @user.password)
65 else
67 else
66 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
68 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
67 return false
69 return false
68 end
70 end
69 else
71 else
70 # Default behaviour, emails from unknown users are ignored
72 # Default behaviour, emails from unknown users are ignored
71 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
73 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
72 return false
74 return false
73 end
75 end
74 end
76 end
75 User.current = @user
77 User.current = @user
76 dispatch
78 dispatch
77 end
79 end
78
80
79 private
81 private
80
82
81 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
83 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
82 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
84 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
83 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
85 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
84
86
85 def dispatch
87 def dispatch
86 headers = [email.in_reply_to, email.references].flatten.compact
88 headers = [email.in_reply_to, email.references].flatten.compact
87 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
89 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
88 klass, object_id = $1, $2.to_i
90 klass, object_id = $1, $2.to_i
89 method_name = "receive_#{klass}_reply"
91 method_name = "receive_#{klass}_reply"
90 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
92 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
91 send method_name, object_id
93 send method_name, object_id
92 else
94 else
93 # ignoring it
95 # ignoring it
94 end
96 end
95 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
97 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
96 receive_issue_reply(m[1].to_i)
98 receive_issue_reply(m[1].to_i)
97 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
99 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
98 receive_message_reply(m[1].to_i)
100 receive_message_reply(m[1].to_i)
99 else
101 else
100 receive_issue
102 receive_issue
101 end
103 end
102 rescue ActiveRecord::RecordInvalid => e
104 rescue ActiveRecord::RecordInvalid => e
103 # TODO: send a email to the user
105 # TODO: send a email to the user
104 logger.error e.message if logger
106 logger.error e.message if logger
105 false
107 false
106 rescue MissingInformation => e
108 rescue MissingInformation => e
107 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
109 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
108 false
110 false
109 rescue UnauthorizedAction => e
111 rescue UnauthorizedAction => e
110 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
112 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
111 false
113 false
112 end
114 end
113
115
114 # Creates a new issue
116 # Creates a new issue
115 def receive_issue
117 def receive_issue
116 project = target_project
118 project = target_project
117 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
119 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
118 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
120 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
119 priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
121 priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
120 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
122 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
121
123
122 # check permission
124 # check permission
123 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
125 unless @@handler_options[:no_permission_check]
126 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
127 end
128
124 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
129 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
125 # check workflow
130 # check workflow
126 if status && issue.new_statuses_allowed_to(user).include?(status)
131 if status && issue.new_statuses_allowed_to(user).include?(status)
127 issue.status = status
132 issue.status = status
128 end
133 end
129 issue.subject = email.subject.chomp
134 issue.subject = email.subject.chomp
130 issue.subject = issue.subject.toutf8 if issue.subject.respond_to?(:toutf8)
135 issue.subject = issue.subject.toutf8 if issue.subject.respond_to?(:toutf8)
131 if issue.subject.blank?
136 if issue.subject.blank?
132 issue.subject = '(no subject)'
137 issue.subject = '(no subject)'
133 end
138 end
134 issue.description = plain_text_body
139 issue.description = plain_text_body
135 # custom fields
140 # custom fields
136 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
141 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
137 if value = get_keyword(c.name, :override => true)
142 if value = get_keyword(c.name, :override => true)
138 h[c.id] = value
143 h[c.id] = value
139 end
144 end
140 h
145 h
141 end
146 end
142 # add To and Cc as watchers before saving so the watchers can reply to Redmine
147 # add To and Cc as watchers before saving so the watchers can reply to Redmine
143 add_watchers(issue)
148 add_watchers(issue)
144 issue.save!
149 issue.save!
145 add_attachments(issue)
150 add_attachments(issue)
146 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
151 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
147 issue
152 issue
148 end
153 end
149
154
150 def target_project
155 def target_project
151 # TODO: other ways to specify project:
156 # TODO: other ways to specify project:
152 # * parse the email To field
157 # * parse the email To field
153 # * specific project (eg. Setting.mail_handler_target_project)
158 # * specific project (eg. Setting.mail_handler_target_project)
154 target = Project.find_by_identifier(get_keyword(:project))
159 target = Project.find_by_identifier(get_keyword(:project))
155 raise MissingInformation.new('Unable to determine target project') if target.nil?
160 raise MissingInformation.new('Unable to determine target project') if target.nil?
156 target
161 target
157 end
162 end
158
163
159 # Adds a note to an existing issue
164 # Adds a note to an existing issue
160 def receive_issue_reply(issue_id)
165 def receive_issue_reply(issue_id)
161 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
166 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
162
167
163 issue = Issue.find_by_id(issue_id)
168 issue = Issue.find_by_id(issue_id)
164 return unless issue
169 return unless issue
165 # check permission
170 # check permission
166 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
171 unless @@handler_options[:no_permission_check]
167 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
172 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
173 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
174 end
168
175
169 # add the note
176 # add the note
170 journal = issue.init_journal(user, plain_text_body)
177 journal = issue.init_journal(user, plain_text_body)
171 add_attachments(issue)
178 add_attachments(issue)
172 # check workflow
179 # check workflow
173 if status && issue.new_statuses_allowed_to(user).include?(status)
180 if status && issue.new_statuses_allowed_to(user).include?(status)
174 issue.status = status
181 issue.status = status
175 end
182 end
176 issue.save!
183 issue.save!
177 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
184 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
178 journal
185 journal
179 end
186 end
180
187
181 # Reply will be added to the issue
188 # Reply will be added to the issue
182 def receive_journal_reply(journal_id)
189 def receive_journal_reply(journal_id)
183 journal = Journal.find_by_id(journal_id)
190 journal = Journal.find_by_id(journal_id)
184 if journal && journal.journalized_type == 'Issue'
191 if journal && journal.journalized_type == 'Issue'
185 receive_issue_reply(journal.journalized_id)
192 receive_issue_reply(journal.journalized_id)
186 end
193 end
187 end
194 end
188
195
189 # Receives a reply to a forum message
196 # Receives a reply to a forum message
190 def receive_message_reply(message_id)
197 def receive_message_reply(message_id)
191 message = Message.find_by_id(message_id)
198 message = Message.find_by_id(message_id)
192 if message
199 if message
193 message = message.root
200 message = message.root
194 if user.allowed_to?(:add_messages, message.project) && !message.locked?
201
202 unless @@handler_options[:no_permission_check]
203 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
204 end
205
206 if !message.locked?
195 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
207 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
196 :content => plain_text_body)
208 :content => plain_text_body)
197 reply.author = user
209 reply.author = user
198 reply.board = message.board
210 reply.board = message.board
199 message.children << reply
211 message.children << reply
200 add_attachments(reply)
212 add_attachments(reply)
201 reply
213 reply
202 else
214 else
203 raise UnauthorizedAction
215 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
204 end
216 end
205 end
217 end
206 end
218 end
207
219
208 def add_attachments(obj)
220 def add_attachments(obj)
209 if email.has_attachments?
221 if email.has_attachments?
210 email.attachments.each do |attachment|
222 email.attachments.each do |attachment|
211 Attachment.create(:container => obj,
223 Attachment.create(:container => obj,
212 :file => attachment,
224 :file => attachment,
213 :author => user,
225 :author => user,
214 :content_type => attachment.content_type)
226 :content_type => attachment.content_type)
215 end
227 end
216 end
228 end
217 end
229 end
218
230
219 # Adds To and Cc as watchers of the given object if the sender has the
231 # Adds To and Cc as watchers of the given object if the sender has the
220 # appropriate permission
232 # appropriate permission
221 def add_watchers(obj)
233 def add_watchers(obj)
222 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
234 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
223 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
235 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
224 unless addresses.empty?
236 unless addresses.empty?
225 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
237 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
226 watchers.each {|w| obj.add_watcher(w)}
238 watchers.each {|w| obj.add_watcher(w)}
227 end
239 end
228 end
240 end
229 end
241 end
230
242
231 def get_keyword(attr, options={})
243 def get_keyword(attr, options={})
232 @keywords ||= {}
244 @keywords ||= {}
233 if @keywords.has_key?(attr)
245 if @keywords.has_key?(attr)
234 @keywords[attr]
246 @keywords[attr]
235 else
247 else
236 @keywords[attr] = begin
248 @keywords[attr] = begin
237 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
249 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
238 $1.strip
250 $1.strip
239 elsif !@@handler_options[:issue][attr].blank?
251 elsif !@@handler_options[:issue][attr].blank?
240 @@handler_options[:issue][attr]
252 @@handler_options[:issue][attr]
241 end
253 end
242 end
254 end
243 end
255 end
244 end
256 end
245
257
246 # Returns the text/plain part of the email
258 # Returns the text/plain part of the email
247 # If not found (eg. HTML-only email), returns the body with tags removed
259 # If not found (eg. HTML-only email), returns the body with tags removed
248 def plain_text_body
260 def plain_text_body
249 return @plain_text_body unless @plain_text_body.nil?
261 return @plain_text_body unless @plain_text_body.nil?
250 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
262 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
251 if parts.empty?
263 if parts.empty?
252 parts << @email
264 parts << @email
253 end
265 end
254 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
266 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
255 if plain_text_part.nil?
267 if plain_text_part.nil?
256 # no text/plain part found, assuming html-only email
268 # no text/plain part found, assuming html-only email
257 # strip html tags and remove doctype directive
269 # strip html tags and remove doctype directive
258 @plain_text_body = strip_tags(@email.body.to_s)
270 @plain_text_body = strip_tags(@email.body.to_s)
259 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
271 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
260 else
272 else
261 @plain_text_body = plain_text_part.body.to_s
273 @plain_text_body = plain_text_part.body.to_s
262 end
274 end
263 @plain_text_body.strip!
275 @plain_text_body.strip!
264 @plain_text_body
276 @plain_text_body
265 end
277 end
266
278
267
279
268 def self.full_sanitizer
280 def self.full_sanitizer
269 @full_sanitizer ||= HTML::FullSanitizer.new
281 @full_sanitizer ||= HTML::FullSanitizer.new
270 end
282 end
271
283
272 # Creates a user account for the +email+ sender
284 # Creates a user account for the +email+ sender
273 def self.create_user_from_email(email)
285 def self.create_user_from_email(email)
274 addr = email.from_addrs.to_a.first
286 addr = email.from_addrs.to_a.first
275 if addr && !addr.spec.blank?
287 if addr && !addr.spec.blank?
276 user = User.new
288 user = User.new
277 user.mail = addr.spec
289 user.mail = addr.spec
278
290
279 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
291 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
280 user.firstname = names.shift
292 user.firstname = names.shift
281 user.lastname = names.join(' ')
293 user.lastname = names.join(' ')
282 user.lastname = '-' if user.lastname.blank?
294 user.lastname = '-' if user.lastname.blank?
283
295
284 user.login = user.mail
296 user.login = user.mail
285 user.password = ActiveSupport::SecureRandom.hex(5)
297 user.password = ActiveSupport::SecureRandom.hex(5)
286 user.language = Setting.default_language
298 user.language = Setting.default_language
287 user.save ? user : nil
299 user.save ? user : nil
288 end
300 end
289 end
301 end
290 end
302 end
@@ -1,159 +1,165
1 #!/usr/bin/env ruby
1 #!/usr/bin/env ruby
2
2
3 # == Synopsis
3 # == Synopsis
4 #
4 #
5 # Reads an email from standard input and forward it to a Redmine server
5 # Reads an email from standard input and forward it to a Redmine server
6 # through a HTTP request.
6 # through a HTTP request.
7 #
7 #
8 # == Usage
8 # == Usage
9 #
9 #
10 # rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>
10 # rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>
11 #
11 #
12 # == Arguments
12 # == Arguments
13 #
13 #
14 # -u, --url URL of the Redmine server
14 # -u, --url URL of the Redmine server
15 # -k, --key Redmine API key
15 # -k, --key Redmine API key
16 #
16 #
17 # General options:
17 # General options:
18 # --unknown-user=ACTION how to handle emails from an unknown user
18 # --unknown-user=ACTION how to handle emails from an unknown user
19 # ACTION can be one of the following values:
19 # ACTION can be one of the following values:
20 # ignore: email is ignored (default)
20 # ignore: email is ignored (default)
21 # accept: accept as anonymous user
21 # accept: accept as anonymous user
22 # create: create a user account
22 # create: create a user account
23 # --no-permission-check disable permission checking when receiving
24 # the email
23 # -h, --help show this help
25 # -h, --help show this help
24 # -v, --verbose show extra information
26 # -v, --verbose show extra information
25 # -V, --version show version information and exit
27 # -V, --version show version information and exit
26 #
28 #
27 # Issue attributes control options:
29 # Issue attributes control options:
28 # -p, --project=PROJECT identifier of the target project
30 # -p, --project=PROJECT identifier of the target project
29 # -s, --status=STATUS name of the target status
31 # -s, --status=STATUS name of the target status
30 # -t, --tracker=TRACKER name of the target tracker
32 # -t, --tracker=TRACKER name of the target tracker
31 # --category=CATEGORY name of the target category
33 # --category=CATEGORY name of the target category
32 # --priority=PRIORITY name of the target priority
34 # --priority=PRIORITY name of the target priority
33 # -o, --allow-override=ATTRS allow email content to override attributes
35 # -o, --allow-override=ATTRS allow email content to override attributes
34 # specified by previous options
36 # specified by previous options
35 # ATTRS is a comma separated list of attributes
37 # ATTRS is a comma separated list of attributes
36 #
38 #
37 # == Examples
39 # == Examples
38 # No project specified. Emails MUST contain the 'Project' keyword:
40 # No project specified. Emails MUST contain the 'Project' keyword:
39 #
41 #
40 # rdm-mailhandler --url http://redmine.domain.foo --key secret
42 # rdm-mailhandler --url http://redmine.domain.foo --key secret
41 #
43 #
42 # Fixed project and default tracker specified, but emails can override
44 # Fixed project and default tracker specified, but emails can override
43 # both tracker and priority attributes using keywords:
45 # both tracker and priority attributes using keywords:
44 #
46 #
45 # rdm-mailhandler --url https://domain.foo/redmine --key secret \\
47 # rdm-mailhandler --url https://domain.foo/redmine --key secret \\
46 # --project foo \\
48 # --project foo \\
47 # --tracker bug \\
49 # --tracker bug \\
48 # --allow-override tracker,priority
50 # --allow-override tracker,priority
49
51
50 require 'net/http'
52 require 'net/http'
51 require 'net/https'
53 require 'net/https'
52 require 'uri'
54 require 'uri'
53 require 'getoptlong'
55 require 'getoptlong'
54 require 'rdoc/usage'
56 require 'rdoc/usage'
55
57
56 module Net
58 module Net
57 class HTTPS < HTTP
59 class HTTPS < HTTP
58 def self.post_form(url, params)
60 def self.post_form(url, params)
59 request = Post.new(url.path)
61 request = Post.new(url.path)
60 request.form_data = params
62 request.form_data = params
61 request.basic_auth url.user, url.password if url.user
63 request.basic_auth url.user, url.password if url.user
62 http = new(url.host, url.port)
64 http = new(url.host, url.port)
63 http.use_ssl = (url.scheme == 'https')
65 http.use_ssl = (url.scheme == 'https')
64 http.start {|h| h.request(request) }
66 http.start {|h| h.request(request) }
65 end
67 end
66 end
68 end
67 end
69 end
68
70
69 class RedmineMailHandler
71 class RedmineMailHandler
70 VERSION = '0.1'
72 VERSION = '0.1'
71
73
72 attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :url, :key
74 attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :no_permission_check, :url, :key
73
75
74 def initialize
76 def initialize
75 self.issue_attributes = {}
77 self.issue_attributes = {}
76
78
77 opts = GetoptLong.new(
79 opts = GetoptLong.new(
78 [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
80 [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
79 [ '--version', '-V', GetoptLong::NO_ARGUMENT ],
81 [ '--version', '-V', GetoptLong::NO_ARGUMENT ],
80 [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
82 [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
81 [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ],
83 [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ],
82 [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT],
84 [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT],
83 [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ],
85 [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ],
84 [ '--status', '-s', GetoptLong::REQUIRED_ARGUMENT ],
86 [ '--status', '-s', GetoptLong::REQUIRED_ARGUMENT ],
85 [ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT],
87 [ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT],
86 [ '--category', GetoptLong::REQUIRED_ARGUMENT],
88 [ '--category', GetoptLong::REQUIRED_ARGUMENT],
87 [ '--priority', GetoptLong::REQUIRED_ARGUMENT],
89 [ '--priority', GetoptLong::REQUIRED_ARGUMENT],
88 [ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT],
90 [ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT],
89 [ '--unknown-user', GetoptLong::REQUIRED_ARGUMENT]
91 [ '--unknown-user', GetoptLong::REQUIRED_ARGUMENT],
92 [ '--no-permission-check', GetoptLong::NO_ARGUMENT]
90 )
93 )
91
94
92 opts.each do |opt, arg|
95 opts.each do |opt, arg|
93 case opt
96 case opt
94 when '--url'
97 when '--url'
95 self.url = arg.dup
98 self.url = arg.dup
96 when '--key'
99 when '--key'
97 self.key = arg.dup
100 self.key = arg.dup
98 when '--help'
101 when '--help'
99 usage
102 usage
100 when '--verbose'
103 when '--verbose'
101 self.verbose = true
104 self.verbose = true
102 when '--version'
105 when '--version'
103 puts VERSION; exit
106 puts VERSION; exit
104 when '--project', '--status', '--tracker', '--category', '--priority'
107 when '--project', '--status', '--tracker', '--category', '--priority'
105 self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup
108 self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup
106 when '--allow-override'
109 when '--allow-override'
107 self.allow_override = arg.dup
110 self.allow_override = arg.dup
108 when '--unknown-user'
111 when '--unknown-user'
109 self.unknown_user = arg.dup
112 self.unknown_user = arg.dup
113 when '--no-permission-check'
114 self.no_permission_check = '1'
110 end
115 end
111 end
116 end
112
117
113 RDoc.usage if url.nil?
118 RDoc.usage if url.nil?
114 end
119 end
115
120
116 def submit(email)
121 def submit(email)
117 uri = url.gsub(%r{/*$}, '') + '/mail_handler'
122 uri = url.gsub(%r{/*$}, '') + '/mail_handler'
118
123
119 data = { 'key' => key, 'email' => email,
124 data = { 'key' => key, 'email' => email,
120 'allow_override' => allow_override,
125 'allow_override' => allow_override,
121 'unknown_user' => unknown_user }
126 'unknown_user' => unknown_user,
127 'no_permission_check' => no_permission_check}
122 issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
128 issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
123
129
124 debug "Posting to #{uri}..."
130 debug "Posting to #{uri}..."
125 response = Net::HTTPS.post_form(URI.parse(uri), data)
131 response = Net::HTTPS.post_form(URI.parse(uri), data)
126 debug "Response received: #{response.code}"
132 debug "Response received: #{response.code}"
127
133
128 case response.code.to_i
134 case response.code.to_i
129 when 403
135 when 403
130 warn "Request was denied by your Redmine server. " +
136 warn "Request was denied by your Redmine server. " +
131 "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
137 "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
132 return 77
138 return 77
133 when 422
139 when 422
134 warn "Request was denied by your Redmine server. " +
140 warn "Request was denied by your Redmine server. " +
135 "Possible reasons: email is sent from an invalid email address or is missing some information."
141 "Possible reasons: email is sent from an invalid email address or is missing some information."
136 return 77
142 return 77
137 when 400..499
143 when 400..499
138 warn "Request was denied by your Redmine server (#{response.code})."
144 warn "Request was denied by your Redmine server (#{response.code})."
139 return 77
145 return 77
140 when 500..599
146 when 500..599
141 warn "Failed to contact your Redmine server (#{response.code})."
147 warn "Failed to contact your Redmine server (#{response.code})."
142 return 75
148 return 75
143 when 201
149 when 201
144 debug "Proccessed successfully"
150 debug "Proccessed successfully"
145 return 0
151 return 0
146 else
152 else
147 return 1
153 return 1
148 end
154 end
149 end
155 end
150
156
151 private
157 private
152
158
153 def debug(msg)
159 def debug(msg)
154 puts msg if verbose
160 puts msg if verbose
155 end
161 end
156 end
162 end
157
163
158 handler = RedmineMailHandler.new
164 handler = RedmineMailHandler.new
159 exit(handler.submit(STDIN.read))
165 exit(handler.submit(STDIN.read))
@@ -1,130 +1,136
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 namespace :redmine do
18 namespace :redmine do
19 namespace :email do
19 namespace :email do
20
20
21 desc <<-END_DESC
21 desc <<-END_DESC
22 Read an email from standard input.
22 Read an email from standard input.
23
23
24 General options:
24 General options:
25 unknown_user=ACTION how to handle emails from an unknown user
25 unknown_user=ACTION how to handle emails from an unknown user
26 ACTION can be one of the following values:
26 ACTION can be one of the following values:
27 ignore: email is ignored (default)
27 ignore: email is ignored (default)
28 accept: accept as anonymous user
28 accept: accept as anonymous user
29 create: create a user account
29 create: create a user account
30 no_permission_check=1 disable permission checking when receiving
31 the email
30
32
31 Issue attributes control options:
33 Issue attributes control options:
32 project=PROJECT identifier of the target project
34 project=PROJECT identifier of the target project
33 status=STATUS name of the target status
35 status=STATUS name of the target status
34 tracker=TRACKER name of the target tracker
36 tracker=TRACKER name of the target tracker
35 category=CATEGORY name of the target category
37 category=CATEGORY name of the target category
36 priority=PRIORITY name of the target priority
38 priority=PRIORITY name of the target priority
37 allow_override=ATTRS allow email content to override attributes
39 allow_override=ATTRS allow email content to override attributes
38 specified by previous options
40 specified by previous options
39 ATTRS is a comma separated list of attributes
41 ATTRS is a comma separated list of attributes
40
42
41 Examples:
43 Examples:
42 # No project specified. Emails MUST contain the 'Project' keyword:
44 # No project specified. Emails MUST contain the 'Project' keyword:
43 rake redmine:email:read RAILS_ENV="production" < raw_email
45 rake redmine:email:read RAILS_ENV="production" < raw_email
44
46
45 # Fixed project and default tracker specified, but emails can override
47 # Fixed project and default tracker specified, but emails can override
46 # both tracker and priority attributes:
48 # both tracker and priority attributes:
47 rake redmine:email:read RAILS_ENV="production" \\
49 rake redmine:email:read RAILS_ENV="production" \\
48 project=foo \\
50 project=foo \\
49 tracker=bug \\
51 tracker=bug \\
50 allow_override=tracker,priority < raw_email
52 allow_override=tracker,priority < raw_email
51 END_DESC
53 END_DESC
52
54
53 task :read => :environment do
55 task :read => :environment do
54 options = { :issue => {} }
56 options = { :issue => {} }
55 %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
57 %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
56 options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
58 options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
57 options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user']
59 options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user']
60 options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check']
58
61
59 MailHandler.receive(STDIN.read, options)
62 MailHandler.receive(STDIN.read, options)
60 end
63 end
61
64
62 desc <<-END_DESC
65 desc <<-END_DESC
63 Read emails from an IMAP server.
66 Read emails from an IMAP server.
64
67
65 General options:
68 General options:
66 unknown_user=ACTION how to handle emails from an unknown user
69 unknown_user=ACTION how to handle emails from an unknown user
67 ACTION can be one of the following values:
70 ACTION can be one of the following values:
68 ignore: email is ignored (default)
71 ignore: email is ignored (default)
69 accept: accept as anonymous user
72 accept: accept as anonymous user
70 create: create a user account
73 create: create a user account
74 no_permission_check=1 disable permission checking when receiving
75 the email
71
76
72 Available IMAP options:
77 Available IMAP options:
73 host=HOST IMAP server host (default: 127.0.0.1)
78 host=HOST IMAP server host (default: 127.0.0.1)
74 port=PORT IMAP server port (default: 143)
79 port=PORT IMAP server port (default: 143)
75 ssl=SSL Use SSL? (default: false)
80 ssl=SSL Use SSL? (default: false)
76 username=USERNAME IMAP account
81 username=USERNAME IMAP account
77 password=PASSWORD IMAP password
82 password=PASSWORD IMAP password
78 folder=FOLDER IMAP folder to read (default: INBOX)
83 folder=FOLDER IMAP folder to read (default: INBOX)
79
84
80 Issue attributes control options:
85 Issue attributes control options:
81 project=PROJECT identifier of the target project
86 project=PROJECT identifier of the target project
82 status=STATUS name of the target status
87 status=STATUS name of the target status
83 tracker=TRACKER name of the target tracker
88 tracker=TRACKER name of the target tracker
84 category=CATEGORY name of the target category
89 category=CATEGORY name of the target category
85 priority=PRIORITY name of the target priority
90 priority=PRIORITY name of the target priority
86 allow_override=ATTRS allow email content to override attributes
91 allow_override=ATTRS allow email content to override attributes
87 specified by previous options
92 specified by previous options
88 ATTRS is a comma separated list of attributes
93 ATTRS is a comma separated list of attributes
89
94
90 Processed emails control options:
95 Processed emails control options:
91 move_on_success=MAILBOX move emails that were successfully received
96 move_on_success=MAILBOX move emails that were successfully received
92 to MAILBOX instead of deleting them
97 to MAILBOX instead of deleting them
93 move_on_failure=MAILBOX move emails that were ignored to MAILBOX
98 move_on_failure=MAILBOX move emails that were ignored to MAILBOX
94
99
95 Examples:
100 Examples:
96 # No project specified. Emails MUST contain the 'Project' keyword:
101 # No project specified. Emails MUST contain the 'Project' keyword:
97
102
98 rake redmine:email:receive_iamp RAILS_ENV="production" \\
103 rake redmine:email:receive_iamp RAILS_ENV="production" \\
99 host=imap.foo.bar username=redmine@example.net password=xxx
104 host=imap.foo.bar username=redmine@example.net password=xxx
100
105
101
106
102 # Fixed project and default tracker specified, but emails can override
107 # Fixed project and default tracker specified, but emails can override
103 # both tracker and priority attributes:
108 # both tracker and priority attributes:
104
109
105 rake redmine:email:receive_iamp RAILS_ENV="production" \\
110 rake redmine:email:receive_iamp RAILS_ENV="production" \\
106 host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\
111 host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\
107 project=foo \\
112 project=foo \\
108 tracker=bug \\
113 tracker=bug \\
109 allow_override=tracker,priority
114 allow_override=tracker,priority
110 END_DESC
115 END_DESC
111
116
112 task :receive_imap => :environment do
117 task :receive_imap => :environment do
113 imap_options = {:host => ENV['host'],
118 imap_options = {:host => ENV['host'],
114 :port => ENV['port'],
119 :port => ENV['port'],
115 :ssl => ENV['ssl'],
120 :ssl => ENV['ssl'],
116 :username => ENV['username'],
121 :username => ENV['username'],
117 :password => ENV['password'],
122 :password => ENV['password'],
118 :folder => ENV['folder'],
123 :folder => ENV['folder'],
119 :move_on_success => ENV['move_on_success'],
124 :move_on_success => ENV['move_on_success'],
120 :move_on_failure => ENV['move_on_failure']}
125 :move_on_failure => ENV['move_on_failure']}
121
126
122 options = { :issue => {} }
127 options = { :issue => {} }
123 %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
128 %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
124 options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
129 options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
125 options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user']
130 options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user']
131 options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check']
126
132
127 Redmine::IMAP.check(imap_options, options)
133 Redmine::IMAP.check(imap_options, options)
128 end
134 end
129 end
135 end
130 end
136 end
@@ -1,268 +1,288
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 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 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class MailHandlerTest < ActiveSupport::TestCase
20 class MailHandlerTest < ActiveSupport::TestCase
21 fixtures :users, :projects,
21 fixtures :users, :projects,
22 :enabled_modules,
22 :enabled_modules,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :workflows,
28 :workflows,
29 :trackers,
29 :trackers,
30 :projects_trackers,
30 :projects_trackers,
31 :enumerations,
31 :enumerations,
32 :issue_categories,
32 :issue_categories,
33 :custom_fields,
33 :custom_fields,
34 :custom_fields_trackers,
34 :custom_fields_trackers,
35 :boards,
35 :boards,
36 :messages
36 :messages
37
37
38 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
38 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
39
39
40 def setup
40 def setup
41 ActionMailer::Base.deliveries.clear
41 ActionMailer::Base.deliveries.clear
42 end
42 end
43
43
44 def test_add_issue
44 def test_add_issue
45 ActionMailer::Base.deliveries.clear
45 ActionMailer::Base.deliveries.clear
46 # This email contains: 'Project: onlinestore'
46 # This email contains: 'Project: onlinestore'
47 issue = submit_email('ticket_on_given_project.eml')
47 issue = submit_email('ticket_on_given_project.eml')
48 assert issue.is_a?(Issue)
48 assert issue.is_a?(Issue)
49 assert !issue.new_record?
49 assert !issue.new_record?
50 issue.reload
50 issue.reload
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 Project.find(2), issue.project
53 assert_equal Project.find(2), issue.project
54 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
54 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
55 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
55 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
56 # keywords should be removed from the email body
56 # keywords should be removed from the email body
57 assert !issue.description.match(/^Project:/i)
57 assert !issue.description.match(/^Project:/i)
58 assert !issue.description.match(/^Status:/i)
58 assert !issue.description.match(/^Status:/i)
59 # Email notification should be sent
59 # Email notification should be sent
60 mail = ActionMailer::Base.deliveries.last
60 mail = ActionMailer::Base.deliveries.last
61 assert_not_nil mail
61 assert_not_nil mail
62 assert mail.subject.include?('New ticket on a given project')
62 assert mail.subject.include?('New ticket on a given project')
63 end
63 end
64
64
65 def test_add_issue_with_status
65 def test_add_issue_with_status
66 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
66 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
67 issue = submit_email('ticket_on_given_project.eml')
67 issue = submit_email('ticket_on_given_project.eml')
68 assert issue.is_a?(Issue)
68 assert issue.is_a?(Issue)
69 assert !issue.new_record?
69 assert !issue.new_record?
70 issue.reload
70 issue.reload
71 assert_equal Project.find(2), issue.project
71 assert_equal Project.find(2), issue.project
72 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
72 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
73 end
73 end
74
74
75 def test_add_issue_with_attributes_override
75 def test_add_issue_with_attributes_override
76 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
76 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
77 assert issue.is_a?(Issue)
77 assert issue.is_a?(Issue)
78 assert !issue.new_record?
78 assert !issue.new_record?
79 issue.reload
79 issue.reload
80 assert_equal 'New ticket on a given project', issue.subject
80 assert_equal 'New ticket on a given project', issue.subject
81 assert_equal User.find_by_login('jsmith'), issue.author
81 assert_equal User.find_by_login('jsmith'), issue.author
82 assert_equal Project.find(2), issue.project
82 assert_equal Project.find(2), issue.project
83 assert_equal 'Feature request', issue.tracker.to_s
83 assert_equal 'Feature request', issue.tracker.to_s
84 assert_equal 'Stock management', issue.category.to_s
84 assert_equal 'Stock management', issue.category.to_s
85 assert_equal 'Urgent', issue.priority.to_s
85 assert_equal 'Urgent', issue.priority.to_s
86 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
86 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
87 end
87 end
88
88
89 def test_add_issue_with_partial_attributes_override
89 def test_add_issue_with_partial_attributes_override
90 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
90 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
91 assert issue.is_a?(Issue)
91 assert issue.is_a?(Issue)
92 assert !issue.new_record?
92 assert !issue.new_record?
93 issue.reload
93 issue.reload
94 assert_equal 'New ticket on a given project', issue.subject
94 assert_equal 'New ticket on a given project', issue.subject
95 assert_equal User.find_by_login('jsmith'), issue.author
95 assert_equal User.find_by_login('jsmith'), issue.author
96 assert_equal Project.find(2), issue.project
96 assert_equal Project.find(2), issue.project
97 assert_equal 'Feature request', issue.tracker.to_s
97 assert_equal 'Feature request', issue.tracker.to_s
98 assert_nil issue.category
98 assert_nil issue.category
99 assert_equal 'High', issue.priority.to_s
99 assert_equal 'High', issue.priority.to_s
100 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
100 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
101 end
101 end
102
102
103 def test_add_issue_with_spaces_between_attribute_and_separator
103 def test_add_issue_with_spaces_between_attribute_and_separator
104 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
104 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
105 assert issue.is_a?(Issue)
105 assert issue.is_a?(Issue)
106 assert !issue.new_record?
106 assert !issue.new_record?
107 issue.reload
107 issue.reload
108 assert_equal 'New ticket on a given project', issue.subject
108 assert_equal 'New ticket on a given project', issue.subject
109 assert_equal User.find_by_login('jsmith'), issue.author
109 assert_equal User.find_by_login('jsmith'), issue.author
110 assert_equal Project.find(2), issue.project
110 assert_equal Project.find(2), issue.project
111 assert_equal 'Feature request', issue.tracker.to_s
111 assert_equal 'Feature request', issue.tracker.to_s
112 assert_equal 'Stock management', issue.category.to_s
112 assert_equal 'Stock management', issue.category.to_s
113 assert_equal 'Urgent', issue.priority.to_s
113 assert_equal 'Urgent', issue.priority.to_s
114 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
114 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
115 end
115 end
116
116
117
117
118 def test_add_issue_with_attachment_to_specific_project
118 def test_add_issue_with_attachment_to_specific_project
119 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
119 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
120 assert issue.is_a?(Issue)
120 assert issue.is_a?(Issue)
121 assert !issue.new_record?
121 assert !issue.new_record?
122 issue.reload
122 issue.reload
123 assert_equal 'Ticket created by email with attachment', issue.subject
123 assert_equal 'Ticket created by email with attachment', issue.subject
124 assert_equal User.find_by_login('jsmith'), issue.author
124 assert_equal User.find_by_login('jsmith'), issue.author
125 assert_equal Project.find(2), issue.project
125 assert_equal Project.find(2), issue.project
126 assert_equal 'This is a new ticket with attachments', issue.description
126 assert_equal 'This is a new ticket with attachments', issue.description
127 # Attachment properties
127 # Attachment properties
128 assert_equal 1, issue.attachments.size
128 assert_equal 1, issue.attachments.size
129 assert_equal 'Paella.jpg', issue.attachments.first.filename
129 assert_equal 'Paella.jpg', issue.attachments.first.filename
130 assert_equal 'image/jpeg', issue.attachments.first.content_type
130 assert_equal 'image/jpeg', issue.attachments.first.content_type
131 assert_equal 10790, issue.attachments.first.filesize
131 assert_equal 10790, issue.attachments.first.filesize
132 end
132 end
133
133
134 def test_add_issue_with_custom_fields
134 def test_add_issue_with_custom_fields
135 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
135 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
136 assert issue.is_a?(Issue)
136 assert issue.is_a?(Issue)
137 assert !issue.new_record?
137 assert !issue.new_record?
138 issue.reload
138 issue.reload
139 assert_equal 'New ticket with custom field values', issue.subject
139 assert_equal 'New ticket with custom field values', issue.subject
140 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
140 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
141 assert !issue.description.match(/^searchable field:/i)
141 assert !issue.description.match(/^searchable field:/i)
142 end
142 end
143
143
144 def test_add_issue_with_cc
144 def test_add_issue_with_cc
145 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
145 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
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 issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
149 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
150 assert_equal 1, issue.watchers.size
150 assert_equal 1, issue.watchers.size
151 end
151 end
152
152
153 def test_add_issue_by_unknown_user
153 def test_add_issue_by_unknown_user
154 assert_no_difference 'User.count' do
154 assert_no_difference 'User.count' do
155 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
155 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
156 end
156 end
157 end
157 end
158
158
159 def test_add_issue_by_anonymous_user
159 def test_add_issue_by_anonymous_user
160 Role.anonymous.add_permission!(:add_issues)
160 Role.anonymous.add_permission!(:add_issues)
161 assert_no_difference 'User.count' do
161 assert_no_difference 'User.count' do
162 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
162 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
163 assert issue.is_a?(Issue)
163 assert issue.is_a?(Issue)
164 assert issue.author.anonymous?
164 assert issue.author.anonymous?
165 end
165 end
166 end
166 end
167
167
168 def test_add_issue_by_anonymous_user_on_private_project
169 Role.anonymous.add_permission!(:add_issues)
170 assert_no_difference 'User.count' do
171 assert_no_difference 'Issue.count' do
172 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
173 end
174 end
175 end
176
177 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
178 assert_no_difference 'User.count' do
179 assert_difference 'Issue.count' do
180 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
181 assert issue.is_a?(Issue)
182 assert issue.author.anonymous?
183 assert !issue.project.is_public?
184 end
185 end
186 end
187
168 def test_add_issue_by_created_user
188 def test_add_issue_by_created_user
169 Setting.default_language = 'en'
189 Setting.default_language = 'en'
170 assert_difference 'User.count' do
190 assert_difference 'User.count' do
171 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
191 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
172 assert issue.is_a?(Issue)
192 assert issue.is_a?(Issue)
173 assert issue.author.active?
193 assert issue.author.active?
174 assert_equal 'john.doe@somenet.foo', issue.author.mail
194 assert_equal 'john.doe@somenet.foo', issue.author.mail
175 assert_equal 'John', issue.author.firstname
195 assert_equal 'John', issue.author.firstname
176 assert_equal 'Doe', issue.author.lastname
196 assert_equal 'Doe', issue.author.lastname
177
197
178 # account information
198 # account information
179 email = ActionMailer::Base.deliveries.first
199 email = ActionMailer::Base.deliveries.first
180 assert_not_nil email
200 assert_not_nil email
181 assert email.subject.include?('account activation')
201 assert email.subject.include?('account activation')
182 login = email.body.match(/\* Login: (.*)$/)[1]
202 login = email.body.match(/\* Login: (.*)$/)[1]
183 password = email.body.match(/\* Password: (.*)$/)[1]
203 password = email.body.match(/\* Password: (.*)$/)[1]
184 assert_equal issue.author, User.try_to_login(login, password)
204 assert_equal issue.author, User.try_to_login(login, password)
185 end
205 end
186 end
206 end
187
207
188 def test_add_issue_without_from_header
208 def test_add_issue_without_from_header
189 Role.anonymous.add_permission!(:add_issues)
209 Role.anonymous.add_permission!(:add_issues)
190 assert_equal false, submit_email('ticket_without_from_header.eml')
210 assert_equal false, submit_email('ticket_without_from_header.eml')
191 end
211 end
192
212
193 def test_should_ignore_emails_from_emission_address
213 def test_should_ignore_emails_from_emission_address
194 Role.anonymous.add_permission!(:add_issues)
214 Role.anonymous.add_permission!(:add_issues)
195 assert_no_difference 'User.count' do
215 assert_no_difference 'User.count' do
196 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
216 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
197 end
217 end
198 end
218 end
199
219
200 def test_add_issue_should_send_email_notification
220 def test_add_issue_should_send_email_notification
201 ActionMailer::Base.deliveries.clear
221 ActionMailer::Base.deliveries.clear
202 # This email contains: 'Project: onlinestore'
222 # This email contains: 'Project: onlinestore'
203 issue = submit_email('ticket_on_given_project.eml')
223 issue = submit_email('ticket_on_given_project.eml')
204 assert issue.is_a?(Issue)
224 assert issue.is_a?(Issue)
205 assert_equal 1, ActionMailer::Base.deliveries.size
225 assert_equal 1, ActionMailer::Base.deliveries.size
206 end
226 end
207
227
208 def test_add_issue_note
228 def test_add_issue_note
209 journal = submit_email('ticket_reply.eml')
229 journal = submit_email('ticket_reply.eml')
210 assert journal.is_a?(Journal)
230 assert journal.is_a?(Journal)
211 assert_equal User.find_by_login('jsmith'), journal.user
231 assert_equal User.find_by_login('jsmith'), journal.user
212 assert_equal Issue.find(2), journal.journalized
232 assert_equal Issue.find(2), journal.journalized
213 assert_match /This is reply/, journal.notes
233 assert_match /This is reply/, journal.notes
214 end
234 end
215
235
216 def test_add_issue_note_with_status_change
236 def test_add_issue_note_with_status_change
217 # This email contains: 'Status: Resolved'
237 # This email contains: 'Status: Resolved'
218 journal = submit_email('ticket_reply_with_status.eml')
238 journal = submit_email('ticket_reply_with_status.eml')
219 assert journal.is_a?(Journal)
239 assert journal.is_a?(Journal)
220 issue = Issue.find(journal.issue.id)
240 issue = Issue.find(journal.issue.id)
221 assert_equal User.find_by_login('jsmith'), journal.user
241 assert_equal User.find_by_login('jsmith'), journal.user
222 assert_equal Issue.find(2), journal.journalized
242 assert_equal Issue.find(2), journal.journalized
223 assert_match /This is reply/, journal.notes
243 assert_match /This is reply/, journal.notes
224 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
244 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
225 end
245 end
226
246
227 def test_add_issue_note_should_send_email_notification
247 def test_add_issue_note_should_send_email_notification
228 ActionMailer::Base.deliveries.clear
248 ActionMailer::Base.deliveries.clear
229 journal = submit_email('ticket_reply.eml')
249 journal = submit_email('ticket_reply.eml')
230 assert journal.is_a?(Journal)
250 assert journal.is_a?(Journal)
231 assert_equal 1, ActionMailer::Base.deliveries.size
251 assert_equal 1, ActionMailer::Base.deliveries.size
232 end
252 end
233
253
234 def test_reply_to_a_message
254 def test_reply_to_a_message
235 m = submit_email('message_reply.eml')
255 m = submit_email('message_reply.eml')
236 assert m.is_a?(Message)
256 assert m.is_a?(Message)
237 assert !m.new_record?
257 assert !m.new_record?
238 m.reload
258 m.reload
239 assert_equal 'Reply via email', m.subject
259 assert_equal 'Reply via email', m.subject
240 # The email replies to message #2 which is part of the thread of message #1
260 # The email replies to message #2 which is part of the thread of message #1
241 assert_equal Message.find(1), m.parent
261 assert_equal Message.find(1), m.parent
242 end
262 end
243
263
244 def test_reply_to_a_message_by_subject
264 def test_reply_to_a_message_by_subject
245 m = submit_email('message_reply_by_subject.eml')
265 m = submit_email('message_reply_by_subject.eml')
246 assert m.is_a?(Message)
266 assert m.is_a?(Message)
247 assert !m.new_record?
267 assert !m.new_record?
248 m.reload
268 m.reload
249 assert_equal 'Reply to the first post', m.subject
269 assert_equal 'Reply to the first post', m.subject
250 assert_equal Message.find(1), m.parent
270 assert_equal Message.find(1), m.parent
251 end
271 end
252
272
253 def test_should_strip_tags_of_html_only_emails
273 def test_should_strip_tags_of_html_only_emails
254 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
274 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
255 assert issue.is_a?(Issue)
275 assert issue.is_a?(Issue)
256 assert !issue.new_record?
276 assert !issue.new_record?
257 issue.reload
277 issue.reload
258 assert_equal 'HTML email', issue.subject
278 assert_equal 'HTML email', issue.subject
259 assert_equal 'This is a html-only email.', issue.description
279 assert_equal 'This is a html-only email.', issue.description
260 end
280 end
261
281
262 private
282 private
263
283
264 def submit_email(filename, options={})
284 def submit_email(filename, options={})
265 raw = IO.read(File.join(FIXTURES_PATH, filename))
285 raw = IO.read(File.join(FIXTURES_PATH, filename))
266 MailHandler.receive(raw, options)
286 MailHandler.receive(raw, options)
267 end
287 end
268 end
288 end
General Comments 0
You need to be logged in to leave comments. Login now