##// END OF EJS Templates
Mail handler: strip tags when receiving a html-only email (#2312)....
Jean-Philippe Lang -
r2134:3bb2fccaf11d
parent child
Show More
@@ -0,0 +1,22
1 x-sender: <jsmith@somenet.foo>
2 x-receiver: <redmine@somenet.foo>
3 Received: from [127.0.0.1] ([127.0.0.1]) by somenet.foo with Quick 'n Easy Mail Server SMTP (1.0.0.0);
4 Sun, 14 Dec 2008 16:18:06 GMT
5 Message-ID: <494531B9.1070709@somenet.foo>
6 Date: Sun, 14 Dec 2008 17:18:01 +0100
7 From: "John Smith" <jsmith@somenet.foo>
8 User-Agent: Thunderbird 2.0.0.18 (Windows/20081105)
9 MIME-Version: 1.0
10 To: redmine@somenet.foo
11 Subject: HTML email
12 Content-Type: text/html; charset=ISO-8859-1
13 Content-Transfer-Encoding: 7bit
14
15 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
16 <html>
17 <head>
18 </head>
19 <body bgcolor="#ffffff" text="#000000">
20 This is a <b>html-only</b> email.<br>
21 </body>
22 </html>
@@ -1,176 +1,186
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 include ActionView::Helpers::SanitizeHelper
19 20
20 21 class UnauthorizedAction < StandardError; end
21 22 class MissingInformation < StandardError; end
22 23
23 24 attr_reader :email, :user
24 25
25 26 def self.receive(email, options={})
26 27 @@handler_options = options.dup
27 28
28 29 @@handler_options[:issue] ||= {}
29 30
30 31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
31 32 @@handler_options[:allow_override] ||= []
32 33 # Project needs to be overridable if not specified
33 34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
34 35 # Status overridable by default
35 36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
36 37 super email
37 38 end
38 39
39 40 # Processes incoming emails
40 41 def receive(email)
41 42 @email = email
42 43 @user = User.active.find_by_mail(email.from.first.to_s.strip)
43 44 unless @user
44 45 # Unknown user => the email is ignored
45 46 # TODO: ability to create the user's account
46 47 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
47 48 return false
48 49 end
49 50 User.current = @user
50 51 dispatch
51 52 end
52 53
53 54 private
54 55
55 56 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
56 57
57 58 def dispatch
58 59 if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
59 60 receive_issue_update(m[1].to_i)
60 61 else
61 62 receive_issue
62 63 end
63 64 rescue ActiveRecord::RecordInvalid => e
64 65 # TODO: send a email to the user
65 66 logger.error e.message if logger
66 67 false
67 68 rescue MissingInformation => e
68 69 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
69 70 false
70 71 rescue UnauthorizedAction => e
71 72 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
72 73 false
73 74 end
74 75
75 76 # Creates a new issue
76 77 def receive_issue
77 78 project = target_project
78 79 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
79 80 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
80 81 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
81 82 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
82 83
83 84 # check permission
84 85 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
85 86 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
86 87 # check workflow
87 88 if status && issue.new_statuses_allowed_to(user).include?(status)
88 89 issue.status = status
89 90 end
90 91 issue.subject = email.subject.chomp.toutf8
91 issue.description = email.plain_text_body.chomp
92 issue.description = plain_text_body
92 93 issue.save!
93 94 add_attachments(issue)
94 95 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
95 96 # send notification before adding watchers since they were cc'ed
96 97 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
97 98 # add To and Cc as watchers
98 99 add_watchers(issue)
99 100
100 101 issue
101 102 end
102 103
103 104 def target_project
104 105 # TODO: other ways to specify project:
105 106 # * parse the email To field
106 107 # * specific project (eg. Setting.mail_handler_target_project)
107 108 target = Project.find_by_identifier(get_keyword(:project))
108 109 raise MissingInformation.new('Unable to determine target project') if target.nil?
109 110 target
110 111 end
111 112
112 113 # Adds a note to an existing issue
113 114 def receive_issue_update(issue_id)
114 115 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
115 116
116 117 issue = Issue.find_by_id(issue_id)
117 118 return unless issue
118 119 # check permission
119 120 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
120 121 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
121 122
122 123 # add the note
123 journal = issue.init_journal(user, email.plain_text_body.chomp)
124 journal = issue.init_journal(user, plain_text_body)
124 125 add_attachments(issue)
125 126 # check workflow
126 127 if status && issue.new_statuses_allowed_to(user).include?(status)
127 128 issue.status = status
128 129 end
129 130 issue.save!
130 131 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
131 132 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
132 133 journal
133 134 end
134 135
135 136 def add_attachments(obj)
136 137 if email.has_attachments?
137 138 email.attachments.each do |attachment|
138 139 Attachment.create(:container => obj,
139 140 :file => attachment,
140 141 :author => user,
141 142 :content_type => attachment.content_type)
142 143 end
143 144 end
144 145 end
145 146
146 147 # Adds To and Cc as watchers of the given object if the sender has the
147 148 # appropriate permission
148 149 def add_watchers(obj)
149 150 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
150 151 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
151 152 unless addresses.empty?
152 153 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
153 154 watchers.each {|w| obj.add_watcher(w)}
154 155 end
155 156 end
156 157 end
157 158
158 159 def get_keyword(attr)
159 if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
160 if @@handler_options[:allow_override].include?(attr.to_s) && plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
160 161 $1.strip
161 162 elsif !@@handler_options[:issue][attr].blank?
162 163 @@handler_options[:issue][attr]
163 164 end
164 165 end
165 end
166
167 class TMail::Mail
168 # Returns body of the first plain text part found if any
166
167 # Returns the text/plain part of the email
168 # If not found (eg. HTML-only email), returns the body with tags removed
169 169 def plain_text_body
170 170 return @plain_text_body unless @plain_text_body.nil?
171 p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
172 plain = p.detect {|c| c.content_type == 'text/plain'}
173 @plain_text_body = plain.nil? ? self.body : plain.body
171 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
172 if parts.empty?
173 parts << @email
174 end
175 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
176 if plain_text_part.nil?
177 # no text/plain part found, assuming html-only email
178 # strip html tags and remove doctype directive
179 @plain_text_body = strip_tags(@email.body.to_s)
180 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
181 else
182 @plain_text_body = plain_text_part.body.to_s
183 end
184 @plain_text_body.strip!
174 185 end
175 186 end
176
@@ -1,139 +1,148
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 26 :issue_statuses,
27 27 :workflows,
28 28 :trackers,
29 29 :projects_trackers,
30 30 :enumerations,
31 31 :issue_categories
32 32
33 33 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
34 34
35 35 def setup
36 36 ActionMailer::Base.deliveries.clear
37 37 end
38 38
39 39 def test_add_issue
40 40 # This email contains: 'Project: onlinestore'
41 41 issue = submit_email('ticket_on_given_project.eml')
42 42 assert issue.is_a?(Issue)
43 43 assert !issue.new_record?
44 44 issue.reload
45 45 assert_equal 'New ticket on a given project', issue.subject
46 46 assert_equal User.find_by_login('jsmith'), issue.author
47 47 assert_equal Project.find(2), issue.project
48 48 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
49 49 end
50 50
51 51 def test_add_issue_with_status
52 52 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
53 53 issue = submit_email('ticket_on_given_project.eml')
54 54 assert issue.is_a?(Issue)
55 55 assert !issue.new_record?
56 56 issue.reload
57 57 assert_equal Project.find(2), issue.project
58 58 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
59 59 end
60 60
61 61 def test_add_issue_with_attributes_override
62 62 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
63 63 assert issue.is_a?(Issue)
64 64 assert !issue.new_record?
65 65 issue.reload
66 66 assert_equal 'New ticket on a given project', issue.subject
67 67 assert_equal User.find_by_login('jsmith'), issue.author
68 68 assert_equal Project.find(2), issue.project
69 69 assert_equal 'Feature request', issue.tracker.to_s
70 70 assert_equal 'Stock management', issue.category.to_s
71 71 assert_equal 'Urgent', issue.priority.to_s
72 72 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
73 73 end
74 74
75 75 def test_add_issue_with_partial_attributes_override
76 76 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
77 77 assert issue.is_a?(Issue)
78 78 assert !issue.new_record?
79 79 issue.reload
80 80 assert_equal 'New ticket on a given project', issue.subject
81 81 assert_equal User.find_by_login('jsmith'), issue.author
82 82 assert_equal Project.find(2), issue.project
83 83 assert_equal 'Feature request', issue.tracker.to_s
84 84 assert_nil issue.category
85 85 assert_equal 'High', issue.priority.to_s
86 86 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
87 87 end
88 88
89 89 def test_add_issue_with_attachment_to_specific_project
90 90 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
91 91 assert issue.is_a?(Issue)
92 92 assert !issue.new_record?
93 93 issue.reload
94 94 assert_equal 'Ticket created by email with attachment', issue.subject
95 95 assert_equal User.find_by_login('jsmith'), issue.author
96 96 assert_equal Project.find(2), issue.project
97 97 assert_equal 'This is a new ticket with attachments', issue.description
98 98 # Attachment properties
99 99 assert_equal 1, issue.attachments.size
100 100 assert_equal 'Paella.jpg', issue.attachments.first.filename
101 101 assert_equal 'image/jpeg', issue.attachments.first.content_type
102 102 assert_equal 10790, issue.attachments.first.filesize
103 103 end
104 104
105 105 def test_add_issue_with_cc
106 106 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
107 107 assert issue.is_a?(Issue)
108 108 assert !issue.new_record?
109 109 issue.reload
110 110 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
111 111 assert_equal 1, issue.watchers.size
112 112 end
113 113
114 114 def test_add_issue_note
115 115 journal = submit_email('ticket_reply.eml')
116 116 assert journal.is_a?(Journal)
117 117 assert_equal User.find_by_login('jsmith'), journal.user
118 118 assert_equal Issue.find(2), journal.journalized
119 119 assert_match /This is reply/, journal.notes
120 120 end
121 121
122 122 def test_add_issue_note_with_status_change
123 123 # This email contains: 'Status: Resolved'
124 124 journal = submit_email('ticket_reply_with_status.eml')
125 125 assert journal.is_a?(Journal)
126 126 issue = Issue.find(journal.issue.id)
127 127 assert_equal User.find_by_login('jsmith'), journal.user
128 128 assert_equal Issue.find(2), journal.journalized
129 129 assert_match /This is reply/, journal.notes
130 130 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
131 131 end
132
133 def test_should_strip_tags_of_html_only_emails
134 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
135 assert issue.is_a?(Issue)
136 assert !issue.new_record?
137 issue.reload
138 assert_equal 'HTML email', issue.subject
139 assert_equal 'This is a html-only email.', issue.description
140 end
132 141
133 142 private
134 143
135 144 def submit_email(filename, options={})
136 145 raw = IO.read(File.join(FIXTURES_PATH, filename))
137 146 MailHandler.receive(raw, options)
138 147 end
139 148 end
General Comments 0
You need to be logged in to leave comments. Login now