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