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