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