##// END OF EJS Templates
Adds To and Cc as watchers when submitting an issue by email (#2245)....
Jean-Philippe Lang -
r2075:84e70634fbd0
parent child
Show More
@@ -0,0 +1,40
1 Return-Path: <JSmith@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 Smith" <JSmith@somenet.foo>
7 To: <redmine@somenet.foo>
8 Cc: <DLopper@somenet.foo>
9 Subject: New ticket on a given project
10 Date: Sun, 22 Jun 2008 12:28:07 +0200
11 MIME-Version: 1.0
12 Content-Type: text/plain;
13 format=flowed;
14 charset="iso-8859-1";
15 reply-type=original
16 Content-Transfer-Encoding: 7bit
17 X-Priority: 3
18 X-MSMail-Priority: Normal
19 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
20 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
21
22 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
23 turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
24 blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
25 sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
26 in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
27 sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
28 id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
29 eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
30 sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
31 malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
32 platea dictumst.
33
34 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
35 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
36 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
37 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
38 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
39 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
40
@@ -1,160 +1,176
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
20 20 class UnauthorizedAction < StandardError; end
21 21 class MissingInformation < StandardError; end
22 22
23 23 attr_reader :email, :user
24 24
25 25 def self.receive(email, options={})
26 26 @@handler_options = options.dup
27 27
28 28 @@handler_options[:issue] ||= {}
29 29
30 30 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
31 31 @@handler_options[:allow_override] ||= []
32 32 # Project needs to be overridable if not specified
33 33 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
34 34 # Status overridable by default
35 35 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
36 36 super email
37 37 end
38 38
39 39 # Processes incoming emails
40 40 def receive(email)
41 41 @email = email
42 42 @user = User.find_active(:first, :conditions => ["LOWER(mail) = ?", email.from.first.to_s.strip.downcase])
43 43 unless @user
44 44 # Unknown user => the email is ignored
45 45 # TODO: ability to create the user's account
46 46 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
47 47 return false
48 48 end
49 49 User.current = @user
50 50 dispatch
51 51 end
52 52
53 53 private
54 54
55 55 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
56 56
57 57 def dispatch
58 58 if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
59 59 receive_issue_update(m[1].to_i)
60 60 else
61 61 receive_issue
62 62 end
63 63 rescue ActiveRecord::RecordInvalid => e
64 64 # TODO: send a email to the user
65 65 logger.error e.message if logger
66 66 false
67 67 rescue MissingInformation => e
68 68 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
69 69 false
70 70 rescue UnauthorizedAction => e
71 71 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
72 72 false
73 73 end
74 74
75 75 # Creates a new issue
76 76 def receive_issue
77 77 project = target_project
78 78 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
79 79 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
80 80 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
81 81 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
82 82
83 83 # check permission
84 84 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
85 85 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
86 86 # check workflow
87 87 if status && issue.new_statuses_allowed_to(user).include?(status)
88 88 issue.status = status
89 89 end
90 90 issue.subject = email.subject.chomp.toutf8
91 91 issue.description = email.plain_text_body.chomp
92 92 issue.save!
93 93 add_attachments(issue)
94 94 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
95 # send notification before adding watchers since they were cc'ed
95 96 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
97 # add To and Cc as watchers
98 add_watchers(issue)
99
96 100 issue
97 101 end
98 102
99 103 def target_project
100 104 # TODO: other ways to specify project:
101 105 # * parse the email To field
102 106 # * specific project (eg. Setting.mail_handler_target_project)
103 107 target = Project.find_by_identifier(get_keyword(:project))
104 108 raise MissingInformation.new('Unable to determine target project') if target.nil?
105 109 target
106 110 end
107 111
108 112 # Adds a note to an existing issue
109 113 def receive_issue_update(issue_id)
110 114 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
111 115
112 116 issue = Issue.find_by_id(issue_id)
113 117 return unless issue
114 118 # check permission
115 119 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
116 120 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
117 121
118 122 # add the note
119 123 journal = issue.init_journal(user, email.plain_text_body.chomp)
120 124 add_attachments(issue)
121 125 # check workflow
122 126 if status && issue.new_statuses_allowed_to(user).include?(status)
123 127 issue.status = status
124 128 end
125 129 issue.save!
126 130 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
127 131 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
128 132 journal
129 133 end
130 134
131 135 def add_attachments(obj)
132 136 if email.has_attachments?
133 137 email.attachments.each do |attachment|
134 138 Attachment.create(:container => obj,
135 139 :file => attachment,
136 140 :author => user,
137 141 :content_type => attachment.content_type)
138 142 end
139 143 end
140 144 end
141 145
146 # Adds To and Cc as watchers of the given object if the sender has the
147 # appropriate permission
148 def add_watchers(obj)
149 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
150 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
151 unless addresses.empty?
152 watchers = User.find_active(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
153 watchers.each {|w| obj.add_watcher(w)}
154 end
155 end
156 end
157
142 158 def get_keyword(attr)
143 159 if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
144 160 $1.strip
145 161 elsif !@@handler_options[:issue][attr].blank?
146 162 @@handler_options[:issue][attr]
147 163 end
148 164 end
149 165 end
150 166
151 167 class TMail::Mail
152 168 # Returns body of the first plain text part found if any
153 169 def plain_text_body
154 170 return @plain_text_body unless @plain_text_body.nil?
155 171 p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
156 172 plain = p.detect {|c| c.content_type == 'text/plain'}
157 173 @plain_text_body = plain.nil? ? self.body : plain.body
158 174 end
159 175 end
160 176
@@ -1,128 +1,139
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 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 :issues,
26 :issue_statuses,
27 :workflows,
26 28 :trackers,
27 29 :projects_trackers,
28 30 :enumerations,
29 31 :issue_categories
30 32
31 33 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
32 34
33 35 def setup
34 36 ActionMailer::Base.deliveries.clear
35 37 end
36 38
37 39 def test_add_issue
38 40 # This email contains: 'Project: onlinestore'
39 41 issue = submit_email('ticket_on_given_project.eml')
40 42 assert issue.is_a?(Issue)
41 43 assert !issue.new_record?
42 44 issue.reload
43 45 assert_equal 'New ticket on a given project', issue.subject
44 46 assert_equal User.find_by_login('jsmith'), issue.author
45 47 assert_equal Project.find(2), issue.project
46 48 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
47 49 end
48 50
49 51 def test_add_issue_with_status
50 52 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
51 53 issue = submit_email('ticket_on_given_project.eml')
52 54 assert issue.is_a?(Issue)
53 55 assert !issue.new_record?
54 56 issue.reload
55 57 assert_equal Project.find(2), issue.project
56 58 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
57 59 end
58 60
59 61 def test_add_issue_with_attributes_override
60 62 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
61 63 assert issue.is_a?(Issue)
62 64 assert !issue.new_record?
63 65 issue.reload
64 66 assert_equal 'New ticket on a given project', issue.subject
65 67 assert_equal User.find_by_login('jsmith'), issue.author
66 68 assert_equal Project.find(2), issue.project
67 69 assert_equal 'Feature request', issue.tracker.to_s
68 70 assert_equal 'Stock management', issue.category.to_s
69 71 assert_equal 'Urgent', issue.priority.to_s
70 72 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
71 73 end
72 74
73 75 def test_add_issue_with_partial_attributes_override
74 76 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
75 77 assert issue.is_a?(Issue)
76 78 assert !issue.new_record?
77 79 issue.reload
78 80 assert_equal 'New ticket on a given project', issue.subject
79 81 assert_equal User.find_by_login('jsmith'), issue.author
80 82 assert_equal Project.find(2), issue.project
81 83 assert_equal 'Feature request', issue.tracker.to_s
82 84 assert_nil issue.category
83 85 assert_equal 'High', issue.priority.to_s
84 86 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
85 87 end
86 88
87 89 def test_add_issue_with_attachment_to_specific_project
88 90 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
89 91 assert issue.is_a?(Issue)
90 92 assert !issue.new_record?
91 93 issue.reload
92 94 assert_equal 'Ticket created by email with attachment', issue.subject
93 95 assert_equal User.find_by_login('jsmith'), issue.author
94 96 assert_equal Project.find(2), issue.project
95 97 assert_equal 'This is a new ticket with attachments', issue.description
96 98 # Attachment properties
97 99 assert_equal 1, issue.attachments.size
98 100 assert_equal 'Paella.jpg', issue.attachments.first.filename
99 101 assert_equal 'image/jpeg', issue.attachments.first.content_type
100 102 assert_equal 10790, issue.attachments.first.filesize
101 103 end
102 104
105 def test_add_issue_with_cc
106 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
107 assert issue.is_a?(Issue)
108 assert !issue.new_record?
109 issue.reload
110 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
111 assert_equal 1, issue.watchers.size
112 end
113
103 114 def test_add_issue_note
104 115 journal = submit_email('ticket_reply.eml')
105 116 assert journal.is_a?(Journal)
106 117 assert_equal User.find_by_login('jsmith'), journal.user
107 118 assert_equal Issue.find(2), journal.journalized
108 119 assert_match /This is reply/, journal.notes
109 120 end
110 121
111 122 def test_add_issue_note_with_status_change
112 123 # This email contains: 'Status: Resolved'
113 124 journal = submit_email('ticket_reply_with_status.eml')
114 125 assert journal.is_a?(Journal)
115 126 issue = Issue.find(journal.issue.id)
116 127 assert_equal User.find_by_login('jsmith'), journal.user
117 128 assert_equal Issue.find(2), journal.journalized
118 129 assert_match /This is reply/, journal.notes
119 130 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
120 131 end
121 132
122 133 private
123 134
124 135 def submit_email(filename, options={})
125 136 raw = IO.read(File.join(FIXTURES_PATH, filename))
126 137 MailHandler.receive(raw, options)
127 138 end
128 139 end
General Comments 0
You need to be logged in to leave comments. Login now