##// END OF EJS Templates
Accept replies to forum messages by subject recognition (#1616)....
Jean-Philippe Lang -
r2292:dfab202cdec1
parent child
Show More
@@ -0,0 +1,13
1 Message-ID: <4974C93E.3070005@somenet.foo>
2 Date: Mon, 19 Jan 2009 19:41:02 +0100
3 From: "John Smith" <jsmith@somenet.foo>
4 User-Agent: Thunderbird 2.0.0.19 (Windows/20081209)
5 MIME-Version: 1.0
6 To: redmine@somenet.foo
7 Subject: Re: [eCookbook - Help board - msg2] Reply to the first post
8 Content-Type: text/plain; charset=UTF-8; format=flowed
9 Content-Transfer-Encoding: 7bit
10
11 This is a reply to a forum message.
12
13
@@ -1,229 +1,232
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 def receive(email)
41 def receive(email)
42 @email = email
42 @email = email
43 @user = User.active.find_by_mail(email.from.first.to_s.strip)
43 @user = User.active.find_by_mail(email.from.first.to_s.strip)
44 unless @user
44 unless @user
45 # Unknown user => the email is ignored
45 # Unknown user => the email is ignored
46 # TODO: ability to create the user's account
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
47 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
48 return false
48 return false
49 end
49 end
50 User.current = @user
50 User.current = @user
51 dispatch
51 dispatch
52 end
52 end
53
53
54 private
54 private
55
55
56 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
56 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
57 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
57 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
58 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
58
59
59 def dispatch
60 def dispatch
60 headers = [email.in_reply_to, email.references].flatten.compact
61 headers = [email.in_reply_to, email.references].flatten.compact
61 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
62 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
62 klass, object_id = $1, $2.to_i
63 klass, object_id = $1, $2.to_i
63 method_name = "receive_#{klass}_reply"
64 method_name = "receive_#{klass}_reply"
64 if self.class.private_instance_methods.include?(method_name)
65 if self.class.private_instance_methods.include?(method_name)
65 send method_name, object_id
66 send method_name, object_id
66 else
67 else
67 # ignoring it
68 # ignoring it
68 end
69 end
69 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
70 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
70 # for compatibility
71 receive_issue_reply(m[1].to_i)
71 receive_issue_reply(m[1].to_i)
72 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
73 receive_message_reply(m[1].to_i)
72 else
74 else
73 receive_issue
75 receive_issue
74 end
76 end
75 rescue ActiveRecord::RecordInvalid => e
77 rescue ActiveRecord::RecordInvalid => e
76 # TODO: send a email to the user
78 # TODO: send a email to the user
77 logger.error e.message if logger
79 logger.error e.message if logger
78 false
80 false
79 rescue MissingInformation => e
81 rescue MissingInformation => e
80 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
82 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
81 false
83 false
82 rescue UnauthorizedAction => e
84 rescue UnauthorizedAction => e
83 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
85 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
84 false
86 false
85 end
87 end
86
88
87 # Creates a new issue
89 # Creates a new issue
88 def receive_issue
90 def receive_issue
89 project = target_project
91 project = target_project
90 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
92 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
91 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
93 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
92 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
94 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
93 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
95 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
94
96
95 # check permission
97 # check permission
96 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
98 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
97 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
99 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
98 # check workflow
100 # check workflow
99 if status && issue.new_statuses_allowed_to(user).include?(status)
101 if status && issue.new_statuses_allowed_to(user).include?(status)
100 issue.status = status
102 issue.status = status
101 end
103 end
102 issue.subject = email.subject.chomp.toutf8
104 issue.subject = email.subject.chomp.toutf8
103 issue.description = plain_text_body
105 issue.description = plain_text_body
104 # custom fields
106 # custom fields
105 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
107 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
106 if value = get_keyword(c.name, :override => true)
108 if value = get_keyword(c.name, :override => true)
107 h[c.id] = value
109 h[c.id] = value
108 end
110 end
109 h
111 h
110 end
112 end
111 issue.save!
113 issue.save!
112 add_attachments(issue)
114 add_attachments(issue)
113 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
115 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
114 # add To and Cc as watchers
116 # add To and Cc as watchers
115 add_watchers(issue)
117 add_watchers(issue)
116 # send notification after adding watchers so that they can reply to Redmine
118 # send notification after adding watchers so that they can reply to Redmine
117 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
119 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
118 issue
120 issue
119 end
121 end
120
122
121 def target_project
123 def target_project
122 # TODO: other ways to specify project:
124 # TODO: other ways to specify project:
123 # * parse the email To field
125 # * parse the email To field
124 # * specific project (eg. Setting.mail_handler_target_project)
126 # * specific project (eg. Setting.mail_handler_target_project)
125 target = Project.find_by_identifier(get_keyword(:project))
127 target = Project.find_by_identifier(get_keyword(:project))
126 raise MissingInformation.new('Unable to determine target project') if target.nil?
128 raise MissingInformation.new('Unable to determine target project') if target.nil?
127 target
129 target
128 end
130 end
129
131
130 # Adds a note to an existing issue
132 # Adds a note to an existing issue
131 def receive_issue_reply(issue_id)
133 def receive_issue_reply(issue_id)
132 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
134 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
133
135
134 issue = Issue.find_by_id(issue_id)
136 issue = Issue.find_by_id(issue_id)
135 return unless issue
137 return unless issue
136 # check permission
138 # check permission
137 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
139 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)
140 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
139
141
140 # add the note
142 # add the note
141 journal = issue.init_journal(user, plain_text_body)
143 journal = issue.init_journal(user, plain_text_body)
142 add_attachments(issue)
144 add_attachments(issue)
143 # check workflow
145 # check workflow
144 if status && issue.new_statuses_allowed_to(user).include?(status)
146 if status && issue.new_statuses_allowed_to(user).include?(status)
145 issue.status = status
147 issue.status = status
146 end
148 end
147 issue.save!
149 issue.save!
148 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
150 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
149 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
151 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
150 journal
152 journal
151 end
153 end
152
154
153 # Reply will be added to the issue
155 # Reply will be added to the issue
154 def receive_journal_reply(journal_id)
156 def receive_journal_reply(journal_id)
155 journal = Journal.find_by_id(journal_id)
157 journal = Journal.find_by_id(journal_id)
156 if journal && journal.journalized_type == 'Issue'
158 if journal && journal.journalized_type == 'Issue'
157 receive_issue_reply(journal.journalized_id)
159 receive_issue_reply(journal.journalized_id)
158 end
160 end
159 end
161 end
160
162
161 # Receives a reply to a forum message
163 # Receives a reply to a forum message
162 def receive_message_reply(message_id)
164 def receive_message_reply(message_id)
163 message = Message.find_by_id(message_id)
165 message = Message.find_by_id(message_id)
164 if message
166 if message
165 message = message.root
167 message = message.root
166 if user.allowed_to?(:add_messages, message.project) && !message.locked?
168 if user.allowed_to?(:add_messages, message.project) && !message.locked?
167 reply = Message.new(:subject => email.subject, :content => plain_text_body)
169 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
170 :content => plain_text_body)
168 reply.author = user
171 reply.author = user
169 reply.board = message.board
172 reply.board = message.board
170 message.children << reply
173 message.children << reply
171 add_attachments(reply)
174 add_attachments(reply)
172 reply
175 reply
173 else
176 else
174 raise UnauthorizedAction
177 raise UnauthorizedAction
175 end
178 end
176 end
179 end
177 end
180 end
178
181
179 def add_attachments(obj)
182 def add_attachments(obj)
180 if email.has_attachments?
183 if email.has_attachments?
181 email.attachments.each do |attachment|
184 email.attachments.each do |attachment|
182 Attachment.create(:container => obj,
185 Attachment.create(:container => obj,
183 :file => attachment,
186 :file => attachment,
184 :author => user,
187 :author => user,
185 :content_type => attachment.content_type)
188 :content_type => attachment.content_type)
186 end
189 end
187 end
190 end
188 end
191 end
189
192
190 # Adds To and Cc as watchers of the given object if the sender has the
193 # Adds To and Cc as watchers of the given object if the sender has the
191 # appropriate permission
194 # appropriate permission
192 def add_watchers(obj)
195 def add_watchers(obj)
193 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
196 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}
197 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
195 unless addresses.empty?
198 unless addresses.empty?
196 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
199 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
197 watchers.each {|w| obj.add_watcher(w)}
200 watchers.each {|w| obj.add_watcher(w)}
198 end
201 end
199 end
202 end
200 end
203 end
201
204
202 def get_keyword(attr, options={})
205 def get_keyword(attr, options={})
203 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
206 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
204 $1.strip
207 $1.strip
205 elsif !@@handler_options[:issue][attr].blank?
208 elsif !@@handler_options[:issue][attr].blank?
206 @@handler_options[:issue][attr]
209 @@handler_options[:issue][attr]
207 end
210 end
208 end
211 end
209
212
210 # Returns the text/plain part of the email
213 # Returns the text/plain part of the email
211 # If not found (eg. HTML-only email), returns the body with tags removed
214 # If not found (eg. HTML-only email), returns the body with tags removed
212 def plain_text_body
215 def plain_text_body
213 return @plain_text_body unless @plain_text_body.nil?
216 return @plain_text_body unless @plain_text_body.nil?
214 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
217 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
215 if parts.empty?
218 if parts.empty?
216 parts << @email
219 parts << @email
217 end
220 end
218 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
221 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
219 if plain_text_part.nil?
222 if plain_text_part.nil?
220 # no text/plain part found, assuming html-only email
223 # no text/plain part found, assuming html-only email
221 # strip html tags and remove doctype directive
224 # strip html tags and remove doctype directive
222 @plain_text_body = strip_tags(@email.body.to_s)
225 @plain_text_body = strip_tags(@email.body.to_s)
223 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
226 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
224 else
227 else
225 @plain_text_body = plain_text_part.body.to_s
228 @plain_text_body = plain_text_part.body.to_s
226 end
229 end
227 @plain_text_body.strip!
230 @plain_text_body.strip!
228 end
231 end
229 end
232 end
@@ -1,297 +1,297
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 Mailer < ActionMailer::Base
18 class Mailer < ActionMailer::Base
19 helper :application
19 helper :application
20 helper :issues
20 helper :issues
21 helper :custom_fields
21 helper :custom_fields
22
22
23 include ActionController::UrlWriter
23 include ActionController::UrlWriter
24
24
25 def issue_add(issue)
25 def issue_add(issue)
26 redmine_headers 'Project' => issue.project.identifier,
26 redmine_headers 'Project' => issue.project.identifier,
27 'Issue-Id' => issue.id,
27 'Issue-Id' => issue.id,
28 'Issue-Author' => issue.author.login
28 'Issue-Author' => issue.author.login
29 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
29 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
30 message_id issue
30 message_id issue
31 recipients issue.recipients
31 recipients issue.recipients
32 cc(issue.watcher_recipients - @recipients)
32 cc(issue.watcher_recipients - @recipients)
33 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
33 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
34 body :issue => issue,
34 body :issue => issue,
35 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
35 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
36 end
36 end
37
37
38 def issue_edit(journal)
38 def issue_edit(journal)
39 issue = journal.journalized
39 issue = journal.journalized
40 redmine_headers 'Project' => issue.project.identifier,
40 redmine_headers 'Project' => issue.project.identifier,
41 'Issue-Id' => issue.id,
41 'Issue-Id' => issue.id,
42 'Issue-Author' => issue.author.login
42 'Issue-Author' => issue.author.login
43 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
43 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
44 message_id journal
44 message_id journal
45 references issue
45 references issue
46 @author = journal.user
46 @author = journal.user
47 recipients issue.recipients
47 recipients issue.recipients
48 # Watchers in cc
48 # Watchers in cc
49 cc(issue.watcher_recipients - @recipients)
49 cc(issue.watcher_recipients - @recipients)
50 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
50 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
51 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
51 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
52 s << issue.subject
52 s << issue.subject
53 subject s
53 subject s
54 body :issue => issue,
54 body :issue => issue,
55 :journal => journal,
55 :journal => journal,
56 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
56 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
57 end
57 end
58
58
59 def reminder(user, issues, days)
59 def reminder(user, issues, days)
60 set_language_if_valid user.language
60 set_language_if_valid user.language
61 recipients user.mail
61 recipients user.mail
62 subject l(:mail_subject_reminder, issues.size)
62 subject l(:mail_subject_reminder, issues.size)
63 body :issues => issues,
63 body :issues => issues,
64 :days => days,
64 :days => days,
65 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
65 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
66 end
66 end
67
67
68 def document_added(document)
68 def document_added(document)
69 redmine_headers 'Project' => document.project.identifier
69 redmine_headers 'Project' => document.project.identifier
70 recipients document.project.recipients
70 recipients document.project.recipients
71 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
71 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
72 body :document => document,
72 body :document => document,
73 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
73 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
74 end
74 end
75
75
76 def attachments_added(attachments)
76 def attachments_added(attachments)
77 container = attachments.first.container
77 container = attachments.first.container
78 added_to = ''
78 added_to = ''
79 added_to_url = ''
79 added_to_url = ''
80 case container.class.name
80 case container.class.name
81 when 'Project'
81 when 'Project'
82 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
82 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
83 added_to = "#{l(:label_project)}: #{container}"
83 added_to = "#{l(:label_project)}: #{container}"
84 when 'Version'
84 when 'Version'
85 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
85 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
86 added_to = "#{l(:label_version)}: #{container.name}"
86 added_to = "#{l(:label_version)}: #{container.name}"
87 when 'Document'
87 when 'Document'
88 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
88 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
89 added_to = "#{l(:label_document)}: #{container.title}"
89 added_to = "#{l(:label_document)}: #{container.title}"
90 end
90 end
91 redmine_headers 'Project' => container.project.identifier
91 redmine_headers 'Project' => container.project.identifier
92 recipients container.project.recipients
92 recipients container.project.recipients
93 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
93 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
94 body :attachments => attachments,
94 body :attachments => attachments,
95 :added_to => added_to,
95 :added_to => added_to,
96 :added_to_url => added_to_url
96 :added_to_url => added_to_url
97 end
97 end
98
98
99 def news_added(news)
99 def news_added(news)
100 redmine_headers 'Project' => news.project.identifier
100 redmine_headers 'Project' => news.project.identifier
101 message_id news
101 message_id news
102 recipients news.project.recipients
102 recipients news.project.recipients
103 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
103 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
104 body :news => news,
104 body :news => news,
105 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
105 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
106 end
106 end
107
107
108 def message_posted(message, recipients)
108 def message_posted(message, recipients)
109 redmine_headers 'Project' => message.project.identifier,
109 redmine_headers 'Project' => message.project.identifier,
110 'Topic-Id' => (message.parent_id || message.id)
110 'Topic-Id' => (message.parent_id || message.id)
111 message_id message
111 message_id message
112 references message.parent unless message.parent.nil?
112 references message.parent unless message.parent.nil?
113 recipients(recipients)
113 recipients(recipients)
114 subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
114 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
115 body :message => message,
115 body :message => message,
116 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
116 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
117 end
117 end
118
118
119 def account_information(user, password)
119 def account_information(user, password)
120 set_language_if_valid user.language
120 set_language_if_valid user.language
121 recipients user.mail
121 recipients user.mail
122 subject l(:mail_subject_register, Setting.app_title)
122 subject l(:mail_subject_register, Setting.app_title)
123 body :user => user,
123 body :user => user,
124 :password => password,
124 :password => password,
125 :login_url => url_for(:controller => 'account', :action => 'login')
125 :login_url => url_for(:controller => 'account', :action => 'login')
126 end
126 end
127
127
128 def account_activation_request(user)
128 def account_activation_request(user)
129 # Send the email to all active administrators
129 # Send the email to all active administrators
130 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
130 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
131 subject l(:mail_subject_account_activation_request, Setting.app_title)
131 subject l(:mail_subject_account_activation_request, Setting.app_title)
132 body :user => user,
132 body :user => user,
133 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
133 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
134 end
134 end
135
135
136 def lost_password(token)
136 def lost_password(token)
137 set_language_if_valid(token.user.language)
137 set_language_if_valid(token.user.language)
138 recipients token.user.mail
138 recipients token.user.mail
139 subject l(:mail_subject_lost_password, Setting.app_title)
139 subject l(:mail_subject_lost_password, Setting.app_title)
140 body :token => token,
140 body :token => token,
141 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
141 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
142 end
142 end
143
143
144 def register(token)
144 def register(token)
145 set_language_if_valid(token.user.language)
145 set_language_if_valid(token.user.language)
146 recipients token.user.mail
146 recipients token.user.mail
147 subject l(:mail_subject_register, Setting.app_title)
147 subject l(:mail_subject_register, Setting.app_title)
148 body :token => token,
148 body :token => token,
149 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
149 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
150 end
150 end
151
151
152 def test(user)
152 def test(user)
153 set_language_if_valid(user.language)
153 set_language_if_valid(user.language)
154 recipients user.mail
154 recipients user.mail
155 subject 'Redmine test'
155 subject 'Redmine test'
156 body :url => url_for(:controller => 'welcome')
156 body :url => url_for(:controller => 'welcome')
157 end
157 end
158
158
159 # Overrides default deliver! method to prevent from sending an email
159 # Overrides default deliver! method to prevent from sending an email
160 # with no recipient, cc or bcc
160 # with no recipient, cc or bcc
161 def deliver!(mail = @mail)
161 def deliver!(mail = @mail)
162 return false if (recipients.nil? || recipients.empty?) &&
162 return false if (recipients.nil? || recipients.empty?) &&
163 (cc.nil? || cc.empty?) &&
163 (cc.nil? || cc.empty?) &&
164 (bcc.nil? || bcc.empty?)
164 (bcc.nil? || bcc.empty?)
165
165
166 # Set Message-Id and References
166 # Set Message-Id and References
167 if @message_id_object
167 if @message_id_object
168 mail.message_id = self.class.message_id_for(@message_id_object)
168 mail.message_id = self.class.message_id_for(@message_id_object)
169 end
169 end
170 if @references_objects
170 if @references_objects
171 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
171 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
172 end
172 end
173 super(mail)
173 super(mail)
174 end
174 end
175
175
176 # Sends reminders to issue assignees
176 # Sends reminders to issue assignees
177 # Available options:
177 # Available options:
178 # * :days => how many days in the future to remind about (defaults to 7)
178 # * :days => how many days in the future to remind about (defaults to 7)
179 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
179 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
180 # * :project => id or identifier of project to process (defaults to all projects)
180 # * :project => id or identifier of project to process (defaults to all projects)
181 def self.reminders(options={})
181 def self.reminders(options={})
182 days = options[:days] || 7
182 days = options[:days] || 7
183 project = options[:project] ? Project.find(options[:project]) : nil
183 project = options[:project] ? Project.find(options[:project]) : nil
184 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
184 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
185
185
186 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
186 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
187 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
187 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
188 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
188 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
189 s << "#{Issue.table_name}.project_id = #{project.id}" if project
189 s << "#{Issue.table_name}.project_id = #{project.id}" if project
190 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
190 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
191
191
192 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
192 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
193 :conditions => s.conditions
193 :conditions => s.conditions
194 ).group_by(&:assigned_to)
194 ).group_by(&:assigned_to)
195 issues_by_assignee.each do |assignee, issues|
195 issues_by_assignee.each do |assignee, issues|
196 deliver_reminder(assignee, issues, days) unless assignee.nil?
196 deliver_reminder(assignee, issues, days) unless assignee.nil?
197 end
197 end
198 end
198 end
199
199
200 private
200 private
201 def initialize_defaults(method_name)
201 def initialize_defaults(method_name)
202 super
202 super
203 set_language_if_valid Setting.default_language
203 set_language_if_valid Setting.default_language
204 from Setting.mail_from
204 from Setting.mail_from
205
205
206 # URL options
206 # URL options
207 h = Setting.host_name
207 h = Setting.host_name
208 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
208 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
209 default_url_options[:host] = h
209 default_url_options[:host] = h
210 default_url_options[:protocol] = Setting.protocol
210 default_url_options[:protocol] = Setting.protocol
211
211
212 # Common headers
212 # Common headers
213 headers 'X-Mailer' => 'Redmine',
213 headers 'X-Mailer' => 'Redmine',
214 'X-Redmine-Host' => Setting.host_name,
214 'X-Redmine-Host' => Setting.host_name,
215 'X-Redmine-Site' => Setting.app_title
215 'X-Redmine-Site' => Setting.app_title
216 end
216 end
217
217
218 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
218 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
219 def redmine_headers(h)
219 def redmine_headers(h)
220 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
220 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
221 end
221 end
222
222
223 # Overrides the create_mail method
223 # Overrides the create_mail method
224 def create_mail
224 def create_mail
225 # Removes the current user from the recipients and cc
225 # Removes the current user from the recipients and cc
226 # if he doesn't want to receive notifications about what he does
226 # if he doesn't want to receive notifications about what he does
227 @author ||= User.current
227 @author ||= User.current
228 if @author.pref[:no_self_notified]
228 if @author.pref[:no_self_notified]
229 recipients.delete(@author.mail) if recipients
229 recipients.delete(@author.mail) if recipients
230 cc.delete(@author.mail) if cc
230 cc.delete(@author.mail) if cc
231 end
231 end
232 # Blind carbon copy recipients
232 # Blind carbon copy recipients
233 if Setting.bcc_recipients?
233 if Setting.bcc_recipients?
234 bcc([recipients, cc].flatten.compact.uniq)
234 bcc([recipients, cc].flatten.compact.uniq)
235 recipients []
235 recipients []
236 cc []
236 cc []
237 end
237 end
238 super
238 super
239 end
239 end
240
240
241 # Renders a message with the corresponding layout
241 # Renders a message with the corresponding layout
242 def render_message(method_name, body)
242 def render_message(method_name, body)
243 layout = method_name.to_s.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
243 layout = method_name.to_s.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
244 body[:content_for_layout] = render(:file => method_name, :body => body)
244 body[:content_for_layout] = render(:file => method_name, :body => body)
245 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
245 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
246 end
246 end
247
247
248 # for the case of plain text only
248 # for the case of plain text only
249 def body(*params)
249 def body(*params)
250 value = super(*params)
250 value = super(*params)
251 if Setting.plain_text_mail?
251 if Setting.plain_text_mail?
252 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
252 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
253 unless String === @body or templates.empty?
253 unless String === @body or templates.empty?
254 template = File.basename(templates.first)
254 template = File.basename(templates.first)
255 @body[:content_for_layout] = render(:file => template, :body => @body)
255 @body[:content_for_layout] = render(:file => template, :body => @body)
256 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
256 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
257 return @body
257 return @body
258 end
258 end
259 end
259 end
260 return value
260 return value
261 end
261 end
262
262
263 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
263 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
264 def self.controller_path
264 def self.controller_path
265 ''
265 ''
266 end unless respond_to?('controller_path')
266 end unless respond_to?('controller_path')
267
267
268 # Returns a predictable Message-Id for the given object
268 # Returns a predictable Message-Id for the given object
269 def self.message_id_for(object)
269 def self.message_id_for(object)
270 # id + timestamp should reduce the odds of a collision
270 # id + timestamp should reduce the odds of a collision
271 # as far as we don't send multiple emails for the same object
271 # as far as we don't send multiple emails for the same object
272 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}"
272 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}"
273 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
273 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
274 host = "#{::Socket.gethostname}.redmine" if host.empty?
274 host = "#{::Socket.gethostname}.redmine" if host.empty?
275 "<#{hash}@#{host}>"
275 "<#{hash}@#{host}>"
276 end
276 end
277
277
278 private
278 private
279
279
280 def message_id(object)
280 def message_id(object)
281 @message_id_object = object
281 @message_id_object = object
282 end
282 end
283
283
284 def references(object)
284 def references(object)
285 @references_objects ||= []
285 @references_objects ||= []
286 @references_objects << object
286 @references_objects << object
287 end
287 end
288 end
288 end
289
289
290 # Patch TMail so that message_id is not overwritten
290 # Patch TMail so that message_id is not overwritten
291 module TMail
291 module TMail
292 class Mail
292 class Mail
293 def add_message_id( fqdn = nil )
293 def add_message_id( fqdn = nil )
294 self.message_id ||= ::TMail::new_message_id(fqdn)
294 self.message_id ||= ::TMail::new_message_id(fqdn)
295 end
295 end
296 end
296 end
297 end
297 end
@@ -1,171 +1,180
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 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 :issues,
25 :issues,
26 :issue_statuses,
26 :issue_statuses,
27 :workflows,
27 :workflows,
28 :trackers,
28 :trackers,
29 :projects_trackers,
29 :projects_trackers,
30 :enumerations,
30 :enumerations,
31 :issue_categories,
31 :issue_categories,
32 :custom_fields,
32 :custom_fields,
33 :custom_fields_trackers,
33 :custom_fields_trackers,
34 :boards,
34 :boards,
35 :messages
35 :messages
36
36
37 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
37 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
38
38
39 def setup
39 def setup
40 ActionMailer::Base.deliveries.clear
40 ActionMailer::Base.deliveries.clear
41 end
41 end
42
42
43 def test_add_issue
43 def test_add_issue
44 # This email contains: 'Project: onlinestore'
44 # This email contains: 'Project: onlinestore'
45 issue = submit_email('ticket_on_given_project.eml')
45 issue = submit_email('ticket_on_given_project.eml')
46 assert issue.is_a?(Issue)
46 assert issue.is_a?(Issue)
47 assert !issue.new_record?
47 assert !issue.new_record?
48 issue.reload
48 issue.reload
49 assert_equal 'New ticket on a given project', issue.subject
49 assert_equal 'New ticket on a given project', issue.subject
50 assert_equal User.find_by_login('jsmith'), issue.author
50 assert_equal User.find_by_login('jsmith'), issue.author
51 assert_equal Project.find(2), issue.project
51 assert_equal Project.find(2), issue.project
52 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
52 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
53 end
53 end
54
54
55 def test_add_issue_with_status
55 def test_add_issue_with_status
56 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
56 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
57 issue = submit_email('ticket_on_given_project.eml')
57 issue = submit_email('ticket_on_given_project.eml')
58 assert issue.is_a?(Issue)
58 assert issue.is_a?(Issue)
59 assert !issue.new_record?
59 assert !issue.new_record?
60 issue.reload
60 issue.reload
61 assert_equal Project.find(2), issue.project
61 assert_equal Project.find(2), issue.project
62 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
62 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
63 end
63 end
64
64
65 def test_add_issue_with_attributes_override
65 def test_add_issue_with_attributes_override
66 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
66 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
67 assert issue.is_a?(Issue)
67 assert issue.is_a?(Issue)
68 assert !issue.new_record?
68 assert !issue.new_record?
69 issue.reload
69 issue.reload
70 assert_equal 'New ticket on a given project', issue.subject
70 assert_equal 'New ticket on a given project', issue.subject
71 assert_equal User.find_by_login('jsmith'), issue.author
71 assert_equal User.find_by_login('jsmith'), issue.author
72 assert_equal Project.find(2), issue.project
72 assert_equal Project.find(2), issue.project
73 assert_equal 'Feature request', issue.tracker.to_s
73 assert_equal 'Feature request', issue.tracker.to_s
74 assert_equal 'Stock management', issue.category.to_s
74 assert_equal 'Stock management', issue.category.to_s
75 assert_equal 'Urgent', issue.priority.to_s
75 assert_equal 'Urgent', issue.priority.to_s
76 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
76 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
77 end
77 end
78
78
79 def test_add_issue_with_partial_attributes_override
79 def test_add_issue_with_partial_attributes_override
80 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
80 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
81 assert issue.is_a?(Issue)
81 assert issue.is_a?(Issue)
82 assert !issue.new_record?
82 assert !issue.new_record?
83 issue.reload
83 issue.reload
84 assert_equal 'New ticket on a given project', issue.subject
84 assert_equal 'New ticket on a given project', issue.subject
85 assert_equal User.find_by_login('jsmith'), issue.author
85 assert_equal User.find_by_login('jsmith'), issue.author
86 assert_equal Project.find(2), issue.project
86 assert_equal Project.find(2), issue.project
87 assert_equal 'Feature request', issue.tracker.to_s
87 assert_equal 'Feature request', issue.tracker.to_s
88 assert_nil issue.category
88 assert_nil issue.category
89 assert_equal 'High', issue.priority.to_s
89 assert_equal 'High', issue.priority.to_s
90 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
90 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
91 end
91 end
92
92
93 def test_add_issue_with_attachment_to_specific_project
93 def test_add_issue_with_attachment_to_specific_project
94 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
94 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
95 assert issue.is_a?(Issue)
95 assert issue.is_a?(Issue)
96 assert !issue.new_record?
96 assert !issue.new_record?
97 issue.reload
97 issue.reload
98 assert_equal 'Ticket created by email with attachment', issue.subject
98 assert_equal 'Ticket created by email with attachment', issue.subject
99 assert_equal User.find_by_login('jsmith'), issue.author
99 assert_equal User.find_by_login('jsmith'), issue.author
100 assert_equal Project.find(2), issue.project
100 assert_equal Project.find(2), issue.project
101 assert_equal 'This is a new ticket with attachments', issue.description
101 assert_equal 'This is a new ticket with attachments', issue.description
102 # Attachment properties
102 # Attachment properties
103 assert_equal 1, issue.attachments.size
103 assert_equal 1, issue.attachments.size
104 assert_equal 'Paella.jpg', issue.attachments.first.filename
104 assert_equal 'Paella.jpg', issue.attachments.first.filename
105 assert_equal 'image/jpeg', issue.attachments.first.content_type
105 assert_equal 'image/jpeg', issue.attachments.first.content_type
106 assert_equal 10790, issue.attachments.first.filesize
106 assert_equal 10790, issue.attachments.first.filesize
107 end
107 end
108
108
109 def test_add_issue_with_custom_fields
109 def test_add_issue_with_custom_fields
110 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
110 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
111 assert issue.is_a?(Issue)
111 assert issue.is_a?(Issue)
112 assert !issue.new_record?
112 assert !issue.new_record?
113 issue.reload
113 issue.reload
114 assert_equal 'New ticket with custom field values', issue.subject
114 assert_equal 'New ticket with custom field values', issue.subject
115 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
115 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
116 end
116 end
117
117
118 def test_add_issue_with_cc
118 def test_add_issue_with_cc
119 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
119 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
120 assert issue.is_a?(Issue)
120 assert issue.is_a?(Issue)
121 assert !issue.new_record?
121 assert !issue.new_record?
122 issue.reload
122 issue.reload
123 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
123 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
124 assert_equal 1, issue.watchers.size
124 assert_equal 1, issue.watchers.size
125 end
125 end
126
126
127 def test_add_issue_note
127 def test_add_issue_note
128 journal = submit_email('ticket_reply.eml')
128 journal = submit_email('ticket_reply.eml')
129 assert journal.is_a?(Journal)
129 assert journal.is_a?(Journal)
130 assert_equal User.find_by_login('jsmith'), journal.user
130 assert_equal User.find_by_login('jsmith'), journal.user
131 assert_equal Issue.find(2), journal.journalized
131 assert_equal Issue.find(2), journal.journalized
132 assert_match /This is reply/, journal.notes
132 assert_match /This is reply/, journal.notes
133 end
133 end
134
134
135 def test_add_issue_note_with_status_change
135 def test_add_issue_note_with_status_change
136 # This email contains: 'Status: Resolved'
136 # This email contains: 'Status: Resolved'
137 journal = submit_email('ticket_reply_with_status.eml')
137 journal = submit_email('ticket_reply_with_status.eml')
138 assert journal.is_a?(Journal)
138 assert journal.is_a?(Journal)
139 issue = Issue.find(journal.issue.id)
139 issue = Issue.find(journal.issue.id)
140 assert_equal User.find_by_login('jsmith'), journal.user
140 assert_equal User.find_by_login('jsmith'), journal.user
141 assert_equal Issue.find(2), journal.journalized
141 assert_equal Issue.find(2), journal.journalized
142 assert_match /This is reply/, journal.notes
142 assert_match /This is reply/, journal.notes
143 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
143 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
144 end
144 end
145
145
146 def test_reply_to_a_message
146 def test_reply_to_a_message
147 m = submit_email('message_reply.eml')
147 m = submit_email('message_reply.eml')
148 assert m.is_a?(Message)
148 assert m.is_a?(Message)
149 assert !m.new_record?
149 assert !m.new_record?
150 m.reload
150 m.reload
151 assert_equal 'Reply via email', m.subject
151 assert_equal 'Reply via email', m.subject
152 # The email replies to message #2 which is part of the thread of message #1
152 # The email replies to message #2 which is part of the thread of message #1
153 assert_equal Message.find(1), m.parent
153 assert_equal Message.find(1), m.parent
154 end
154 end
155
155
156 def test_reply_to_a_message_by_subject
157 m = submit_email('message_reply_by_subject.eml')
158 assert m.is_a?(Message)
159 assert !m.new_record?
160 m.reload
161 assert_equal 'Reply to the first post', m.subject
162 assert_equal Message.find(1), m.parent
163 end
164
156 def test_should_strip_tags_of_html_only_emails
165 def test_should_strip_tags_of_html_only_emails
157 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
166 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
158 assert issue.is_a?(Issue)
167 assert issue.is_a?(Issue)
159 assert !issue.new_record?
168 assert !issue.new_record?
160 issue.reload
169 issue.reload
161 assert_equal 'HTML email', issue.subject
170 assert_equal 'HTML email', issue.subject
162 assert_equal 'This is a html-only email.', issue.description
171 assert_equal 'This is a html-only email.', issue.description
163 end
172 end
164
173
165 private
174 private
166
175
167 def submit_email(filename, options={})
176 def submit_email(filename, options={})
168 raw = IO.read(File.join(FIXTURES_PATH, filename))
177 raw = IO.read(File.join(FIXTURES_PATH, filename))
169 MailHandler.receive(raw, options)
178 MailHandler.receive(raw, options)
170 end
179 end
171 end
180 end
General Comments 0
You need to be logged in to leave comments. Login now