@@ -0,0 +1,43 | |||||
|
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 | Subject: New ticket on a given project | |||
|
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 | X-Priority: 3 | |||
|
17 | X-MSMail-Priority: Normal | |||
|
18 | X-Mailer: Microsoft Outlook Express 6.00.2900.2869 | |||
|
19 | X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 | |||
|
20 | ||||
|
21 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet | |||
|
22 | turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus | |||
|
23 | blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti | |||
|
24 | sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In | |||
|
25 | in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras | |||
|
26 | sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum | |||
|
27 | id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus | |||
|
28 | eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique | |||
|
29 | sed, mauris. Pellentesque habitant morbi tristique senectus et netus et | |||
|
30 | malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse | |||
|
31 | platea dictumst. | |||
|
32 | ||||
|
33 | Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque | |||
|
34 | sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. | |||
|
35 | Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, | |||
|
36 | dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, | |||
|
37 | massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo | |||
|
38 | pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. | |||
|
39 | ||||
|
40 | Project: onlinestore | |||
|
41 | Tracker: Feature request | |||
|
42 | category: Stock management | |||
|
43 | priority: Urgent |
@@ -1,134 +1,145 | |||||
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 |
|
19 | |||
20 | class UnauthorizedAction < StandardError; end |
|
20 | class UnauthorizedAction < StandardError; end | |
21 | class MissingInformation < StandardError; end |
|
21 | class MissingInformation < StandardError; end | |
22 |
|
22 | |||
23 | attr_reader :email, :user |
|
23 | attr_reader :email, :user | |
24 |
|
24 | |||
25 | def self.receive(email, options={}) |
|
25 | def self.receive(email, options={}) | |
26 | @@handler_options = options |
|
26 | @@handler_options = options.dup | |
|
27 | ||||
|
28 | @@handler_options[:issue] ||= {} | |||
|
29 | ||||
|
30 | @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String) | |||
|
31 | @@handler_options[:allow_override] ||= [] | |||
|
32 | # Project needs to be overridable if not specified | |||
|
33 | @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) | |||
27 | super email |
|
34 | super email | |
28 | end |
|
35 | end | |
29 |
|
36 | |||
30 | # Processes incoming emails |
|
37 | # Processes incoming emails | |
31 | def receive(email) |
|
38 | def receive(email) | |
32 | @email = email |
|
39 | @email = email | |
33 | @user = User.find_active(:first, :conditions => {:mail => email.from.first}) |
|
40 | @user = User.find_active(:first, :conditions => {:mail => email.from.first}) | |
34 | unless @user |
|
41 | unless @user | |
35 | # Unknown user => the email is ignored |
|
42 | # Unknown user => the email is ignored | |
36 | # TODO: ability to create the user's account |
|
43 | # TODO: ability to create the user's account | |
37 | logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info |
|
44 | logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info | |
38 | return false |
|
45 | return false | |
39 | end |
|
46 | end | |
40 | User.current = @user |
|
47 | User.current = @user | |
41 | dispatch |
|
48 | dispatch | |
42 | end |
|
49 | end | |
43 |
|
50 | |||
44 | private |
|
51 | private | |
45 |
|
52 | |||
46 | ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]} |
|
53 | ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]} | |
47 |
|
54 | |||
48 | def dispatch |
|
55 | def dispatch | |
49 | if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE) |
|
56 | if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE) | |
50 | receive_issue_update(m[1].to_i) |
|
57 | receive_issue_update(m[1].to_i) | |
51 | else |
|
58 | else | |
52 | receive_issue |
|
59 | receive_issue | |
53 | end |
|
60 | end | |
54 | rescue ActiveRecord::RecordInvalid => e |
|
61 | rescue ActiveRecord::RecordInvalid => e | |
55 | # TODO: send a email to the user |
|
62 | # TODO: send a email to the user | |
56 | logger.error e.message if logger |
|
63 | logger.error e.message if logger | |
57 | false |
|
64 | false | |
58 | rescue MissingInformation => e |
|
65 | rescue MissingInformation => e | |
59 | logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger |
|
66 | logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger | |
60 | false |
|
67 | false | |
61 | rescue UnauthorizedAction => e |
|
68 | rescue UnauthorizedAction => e | |
62 | logger.error "MailHandler: unauthorized attempt from #{user}" if logger |
|
69 | logger.error "MailHandler: unauthorized attempt from #{user}" if logger | |
63 | false |
|
70 | false | |
64 | end |
|
71 | end | |
65 |
|
72 | |||
66 | # Creates a new issue |
|
73 | # Creates a new issue | |
67 | def receive_issue |
|
74 | def receive_issue | |
68 | project = target_project |
|
75 | project = target_project | |
69 | # TODO: make the tracker configurable |
|
76 | tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first) | |
70 | tracker = project.trackers.find(:first) |
|
77 | category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category))) | |
|
78 | priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority))) | |||
|
79 | ||||
71 | # check permission |
|
80 | # check permission | |
72 | raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) |
|
81 | raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) | |
73 | issue = Issue.new(:author => user, :project => project, :tracker => tracker) |
|
82 | issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority) | |
74 | issue.subject = email.subject.chomp |
|
83 | issue.subject = email.subject.chomp | |
75 | issue.description = email.plain_text_body.chomp |
|
84 | issue.description = email.plain_text_body.chomp | |
76 | issue.save! |
|
85 | issue.save! | |
77 | add_attachments(issue) |
|
86 | add_attachments(issue) | |
78 | logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info |
|
87 | logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info | |
79 | Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added') |
|
88 | Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added') | |
80 | issue |
|
89 | issue | |
81 | end |
|
90 | end | |
82 |
|
91 | |||
83 | def target_project |
|
92 | def target_project | |
84 | # TODO: other ways to specify project: |
|
93 | # TODO: other ways to specify project: | |
85 | # * parse the email To field |
|
94 | # * parse the email To field | |
86 | # * specific project (eg. Setting.mail_handler_target_project) |
|
95 | # * specific project (eg. Setting.mail_handler_target_project) | |
87 | identifier = if !@@handler_options[:project].blank? |
|
96 | target = Project.find_by_identifier(get_keyword(:project)) | |
88 | @@handler_options[:project] |
|
|||
89 | elsif email.plain_text_body =~ %r{^Project:[ \t]*(.+)$}i |
|
|||
90 | $1 |
|
|||
91 | end |
|
|||
92 |
|
||||
93 | target = Project.find_by_identifier(identifier.to_s) |
|
|||
94 | raise MissingInformation.new('Unable to determine target project') if target.nil? |
|
97 | raise MissingInformation.new('Unable to determine target project') if target.nil? | |
95 | target |
|
98 | target | |
96 | end |
|
99 | end | |
97 |
|
100 | |||
98 | # Adds a note to an existing issue |
|
101 | # Adds a note to an existing issue | |
99 | def receive_issue_update(issue_id) |
|
102 | def receive_issue_update(issue_id) | |
100 | issue = Issue.find_by_id(issue_id) |
|
103 | issue = Issue.find_by_id(issue_id) | |
101 | return unless issue |
|
104 | return unless issue | |
102 | # check permission |
|
105 | # check permission | |
103 | raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) |
|
106 | raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) | |
104 | # add the note |
|
107 | # add the note | |
105 | journal = issue.init_journal(user, email.plain_text_body.chomp) |
|
108 | journal = issue.init_journal(user, email.plain_text_body.chomp) | |
106 | add_attachments(issue) |
|
109 | add_attachments(issue) | |
107 | issue.save! |
|
110 | issue.save! | |
108 | logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info |
|
111 | logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info | |
109 | Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') |
|
112 | Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') | |
110 | journal |
|
113 | journal | |
111 | end |
|
114 | end | |
112 |
|
115 | |||
113 | def add_attachments(obj) |
|
116 | def add_attachments(obj) | |
114 | if email.has_attachments? |
|
117 | if email.has_attachments? | |
115 | email.attachments.each do |attachment| |
|
118 | email.attachments.each do |attachment| | |
116 | Attachment.create(:container => obj, |
|
119 | Attachment.create(:container => obj, | |
117 | :file => attachment, |
|
120 | :file => attachment, | |
118 | :author => user, |
|
121 | :author => user, | |
119 | :content_type => attachment.content_type) |
|
122 | :content_type => attachment.content_type) | |
120 | end |
|
123 | end | |
121 | end |
|
124 | end | |
122 | end |
|
125 | end | |
|
126 | ||||
|
127 | def get_keyword(attr) | |||
|
128 | if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i | |||
|
129 | $1.strip | |||
|
130 | elsif !@@handler_options[:issue][attr].blank? | |||
|
131 | @@handler_options[:issue][attr] | |||
|
132 | end | |||
|
133 | end | |||
123 | end |
|
134 | end | |
124 |
|
135 | |||
125 | class TMail::Mail |
|
136 | class TMail::Mail | |
126 | # Returns body of the first plain text part found if any |
|
137 | # Returns body of the first plain text part found if any | |
127 | def plain_text_body |
|
138 | def plain_text_body | |
128 | return @plain_text_body unless @plain_text_body.nil? |
|
139 | return @plain_text_body unless @plain_text_body.nil? | |
129 | p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten |
|
140 | p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten | |
130 | plain = p.detect {|c| c.content_type == 'text/plain'} |
|
141 | plain = p.detect {|c| c.content_type == 'text/plain'} | |
131 | @plain_text_body = plain.nil? ? self.body : plain.body |
|
142 | @plain_text_body = plain.nil? ? self.body : plain.body | |
132 | end |
|
143 | end | |
133 | end |
|
144 | end | |
134 |
|
145 |
@@ -1,79 +1,112 | |||||
1 | #!/usr/bin/ruby |
|
1 | #!/usr/bin/ruby | |
2 |
|
2 | |||
3 | # rdm-mailhandler |
|
3 | # rdm-mailhandler | |
4 | # Reads an email from standard input and forward it to a Redmine server |
|
4 | # Reads an email from standard input and forward it to a Redmine server | |
5 | # Can be used from a remote mail server |
|
5 | # Can be used from a remote mail server | |
6 |
|
6 | |||
7 | require 'net/http' |
|
7 | require 'net/http' | |
8 | require 'net/https' |
|
8 | require 'net/https' | |
9 | require 'uri' |
|
9 | require 'uri' | |
10 | require 'getoptlong' |
|
10 | require 'getoptlong' | |
11 |
|
11 | |||
12 | class RedmineMailHandler |
|
12 | class RedmineMailHandler | |
13 | VERSION = '0.1' |
|
13 | VERSION = '0.1' | |
14 |
|
14 | |||
15 |
attr_accessor :verbose, : |
|
15 | attr_accessor :verbose, :issue_attributes, :allow_override, :url, :key | |
16 |
|
16 | |||
17 | def initialize |
|
17 | def initialize | |
|
18 | self.issue_attributes = {} | |||
|
19 | ||||
18 | opts = GetoptLong.new( |
|
20 | opts = GetoptLong.new( | |
19 | [ '--help', '-h', GetoptLong::NO_ARGUMENT ], |
|
21 | [ '--help', '-h', GetoptLong::NO_ARGUMENT ], | |
20 | [ '--version', '-V', GetoptLong::NO_ARGUMENT ], |
|
22 | [ '--version', '-V', GetoptLong::NO_ARGUMENT ], | |
21 | [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], |
|
23 | [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], | |
22 | [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ], |
|
24 | [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ], | |
23 | [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT], |
|
25 | [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT], | |
24 | [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ] |
|
26 | [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ], | |
|
27 | [ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT], | |||
|
28 | [ '--category', GetoptLong::REQUIRED_ARGUMENT], | |||
|
29 | [ '--priority', GetoptLong::REQUIRED_ARGUMENT], | |||
|
30 | [ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT] | |||
25 | ) |
|
31 | ) | |
26 |
|
32 | |||
27 | opts.each do |opt, arg| |
|
33 | opts.each do |opt, arg| | |
28 | case opt |
|
34 | case opt | |
29 | when '--url' |
|
35 | when '--url' | |
30 | self.url = arg.dup |
|
36 | self.url = arg.dup | |
31 | when '--key' |
|
37 | when '--key' | |
32 | self.key = arg.dup |
|
38 | self.key = arg.dup | |
33 | when '--help' |
|
39 | when '--help' | |
34 | usage |
|
40 | usage | |
35 | when '--verbose' |
|
41 | when '--verbose' | |
36 | self.verbose = true |
|
42 | self.verbose = true | |
37 | when '--version' |
|
43 | when '--version' | |
38 | puts VERSION; exit |
|
44 | puts VERSION; exit | |
39 | when '--project' |
|
45 | when '--project', '--tracker', '--category', '--priority' | |
40 | self.project = arg.dup |
|
46 | self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup | |
|
47 | when '--allow-override' | |||
|
48 | self.allow_override = arg.dup | |||
41 | end |
|
49 | end | |
42 | end |
|
50 | end | |
43 |
|
51 | |||
44 | usage if url.nil? |
|
52 | usage if url.nil? | |
45 | end |
|
53 | end | |
46 |
|
54 | |||
47 | def submit(email) |
|
55 | def submit(email) | |
48 | uri = url.gsub(%r{/*$}, '') + '/mail_handler' |
|
56 | uri = url.gsub(%r{/*$}, '') + '/mail_handler' | |
|
57 | ||||
|
58 | data = { 'key' => key, 'email' => email, 'allow_override' => allow_override } | |||
|
59 | issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value } | |||
|
60 | ||||
49 | debug "Posting to #{uri}..." |
|
61 | debug "Posting to #{uri}..." | |
50 | data = { 'key' => key, 'project' => project, 'email' => email } |
|
|||
51 | response = Net::HTTP.post_form(URI.parse(uri), data) |
|
62 | response = Net::HTTP.post_form(URI.parse(uri), data) | |
52 | debug "Response received: #{response.code}" |
|
63 | debug "Response received: #{response.code}" | |
53 | response.code == 201 ? 0 : 1 |
|
64 | response.code == 201 ? 0 : 1 | |
54 | end |
|
65 | end | |
55 |
|
66 | |||
56 | private |
|
67 | private | |
57 |
|
68 | |||
58 | def usage |
|
69 | def usage | |
59 | puts "Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>" |
|
70 | puts <<-USAGE | |
60 | puts "Reads an email from standard input and forward it to a Redmine server" |
|
71 | Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key> | |
61 | puts |
|
72 | Reads an email from standard input and forward it to a Redmine server | |
62 | puts "Options:" |
|
73 | ||
63 | puts " --help show this help" |
|
74 | Required: | |
64 | puts " --verbose show extra information" |
|
75 | -u, --url URL of the Redmine server | |
65 | puts " --project identifier of the target project" |
|
76 | -k, --key Redmine API key | |
66 | puts |
|
77 | ||
67 | puts "Examples:" |
|
78 | General options: | |
68 | puts " rdm-mailhandler --url http://redmine.domain.foo --key secret" |
|
79 | -h, --help show this help | |
69 | puts " rdm-mailhandler --url https://redmine.domain.foo --key secret --project foo" |
|
80 | -v, --verbose show extra information | |
|
81 | -V, --version show version information and exit | |||
|
82 | ||||
|
83 | Issue attributes control options: | |||
|
84 | -p, --project=PROJECT identifier of the target project | |||
|
85 | -t, --tracker=TRACKER name of the target tracker | |||
|
86 | --category=CATEGORY name of the target category | |||
|
87 | --priority=PRIORITY name of the target priority | |||
|
88 | -o, --allow-override=ATTRS allow email content to override attributes | |||
|
89 | specified by previous options | |||
|
90 | ATTRS is a comma separated list of attributes | |||
|
91 | ||||
|
92 | Examples: | |||
|
93 | # No project specified. Emails MUST contain the 'Project' keyword: | |||
|
94 | rdm-mailhandler --url http://redmine.domain.foo --key secret | |||
|
95 | ||||
|
96 | # Fixed project and default tracker specified, but emails can override | |||
|
97 | # both tracker and priority attributes: | |||
|
98 | rdm-mailhandler --url https://domain.foo/redmine --key secret \\ | |||
|
99 | --project foo \\ | |||
|
100 | --tracker bug \\ | |||
|
101 | --allow-override tracker,priority | |||
|
102 | USAGE | |||
70 | exit |
|
103 | exit | |
71 | end |
|
104 | end | |
72 |
|
105 | |||
73 | def debug(msg) |
|
106 | def debug(msg) | |
74 | puts msg if verbose |
|
107 | puts msg if verbose | |
75 | end |
|
108 | end | |
76 | end |
|
109 | end | |
77 |
|
110 | |||
78 | handler = RedmineMailHandler.new |
|
111 | handler = RedmineMailHandler.new | |
79 | handler.submit(STDIN.read) |
|
112 | handler.submit(STDIN.read) |
@@ -1,69 +1,105 | |||||
1 | # redMine - project management software |
|
1 | # redMine - project management software | |
2 | # Copyright (C) 2006-2008 Jean-Philippe Lang |
|
2 | # Copyright (C) 2006-2008 Jean-Philippe Lang | |
3 | # |
|
3 | # | |
4 | # This program is free software; you can redistribute it and/or |
|
4 | # This program is free software; you can redistribute it and/or | |
5 | # modify it under the terms of the GNU General Public License |
|
5 | # modify it under the terms of the GNU General Public License | |
6 | # as published by the Free Software Foundation; either version 2 |
|
6 | # as published by the Free Software Foundation; either version 2 | |
7 | # of the License, or (at your option) any later version. |
|
7 | # of the License, or (at your option) any later version. | |
8 | # |
|
8 | # | |
9 | # This program is distributed in the hope that it will be useful, |
|
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU General Public License for more details. |
|
12 | # GNU General Public License for more details. | |
13 | # |
|
13 | # | |
14 | # You should have received a copy of the GNU General Public License |
|
14 | # You should have received a copy of the GNU General Public License | |
15 | # along with this program; if not, write to the Free Software |
|
15 | # along with this program; if not, write to the Free Software | |
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
17 |
|
17 | |||
18 | namespace :redmine do |
|
18 | namespace :redmine do | |
19 | namespace :email do |
|
19 | namespace :email do | |
20 |
|
20 | |||
21 | desc <<-END_DESC |
|
21 | desc <<-END_DESC | |
22 | Read an email from standard input. |
|
22 | Read an email from standard input. | |
23 |
|
23 | |||
24 | Available options: |
|
24 | Issue attributes control options: | |
25 | * project => identifier of the project the issue should be added to |
|
25 | project=PROJECT identifier of the target project | |
26 |
|
26 | tracker=TRACKER name of the target tracker | ||
27 | Example: |
|
27 | category=CATEGORY name of the target category | |
28 | rake redmine:email:receive project=foo RAILS_ENV="production" |
|
28 | priority=PRIORITY name of the target priority | |
|
29 | allow_override=ATTRS allow email content to override attributes | |||
|
30 | specified by previous options | |||
|
31 | ATTRS is a comma separated list of attributes | |||
|
32 | ||||
|
33 | Examples: | |||
|
34 | # No project specified. Emails MUST contain the 'Project' keyword: | |||
|
35 | rake redmine:email:read RAILS_ENV="production" < raw_email | |||
|
36 | ||||
|
37 | # Fixed project and default tracker specified, but emails can override | |||
|
38 | # both tracker and priority attributes: | |||
|
39 | rake redmine:email:read RAILS_ENV="production" \\ | |||
|
40 | project=foo \\ | |||
|
41 | tracker=bug \\ | |||
|
42 | allow_override=tracker,priority < raw_email | |||
29 | END_DESC |
|
43 | END_DESC | |
30 |
|
44 | |||
31 |
task :re |
|
45 | task :read => :environment do | |
32 | options = {} |
|
46 | options = { :issue => {} } | |
33 | options[:project] = ENV['project'] if ENV['project'] |
|
47 | %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } | |
|
48 | options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] | |||
34 |
|
49 | |||
35 | MailHandler.receive(STDIN.read, options) |
|
50 | MailHandler.receive(STDIN.read, options) | |
36 | end |
|
51 | end | |
37 |
|
52 | |||
38 | desc <<-END_DESC |
|
53 | desc <<-END_DESC | |
39 | Read emails from an IMAP server. |
|
54 | Read emails from an IMAP server. | |
40 |
|
55 | |||
41 | Available IMAP options: |
|
56 | Available IMAP options: | |
42 |
|
|
57 | host=HOST IMAP server host (default: 127.0.0.1) | |
43 |
|
|
58 | port=PORT IMAP server port (default: 143) | |
44 |
|
|
59 | ssl=SSL Use SSL? (default: false) | |
45 |
|
|
60 | username=USERNAME IMAP account | |
46 |
|
|
61 | password=PASSWORD IMAP password | |
47 |
|
|
62 | folder=FOLDER IMAP folder to read (default: INBOX) | |
48 | Other options: |
|
63 | ||
49 | * project => identifier of the project the issue should be added to |
|
64 | Issue attributes control options: | |
|
65 | project=PROJECT identifier of the target project | |||
|
66 | tracker=TRACKER name of the target tracker | |||
|
67 | category=CATEGORY name of the target category | |||
|
68 | priority=PRIORITY name of the target priority | |||
|
69 | allow_override=ATTRS allow email content to override attributes | |||
|
70 | specified by previous options | |||
|
71 | ATTRS is a comma separated list of attributes | |||
|
72 | ||||
|
73 | Examples: | |||
|
74 | # No project specified. Emails MUST contain the 'Project' keyword: | |||
|
75 | ||||
|
76 | rake redmine:email:receive_iamp RAILS_ENV="production" \\ | |||
|
77 | host=imap.foo.bar username=redmine@somenet.foo password=xxx | |||
|
78 | ||||
|
79 | ||||
|
80 | # Fixed project and default tracker specified, but emails can override | |||
|
81 | # both tracker and priority attributes: | |||
50 |
|
82 | |||
51 | Example: |
|
83 | rake redmine:email:receive_iamp RAILS_ENV="production" \\ | |
52 |
|
|
84 | host=imap.foo.bar username=redmine@somenet.foo password=xxx ssl=1 \\ | |
|
85 | project=foo \\ | |||
|
86 | tracker=bug \\ | |||
|
87 | allow_override=tracker,priority | |||
53 | END_DESC |
|
88 | END_DESC | |
54 |
|
89 | |||
55 | task :receive_imap => :environment do |
|
90 | task :receive_imap => :environment do | |
56 | imap_options = {:host => ENV['host'], |
|
91 | imap_options = {:host => ENV['host'], | |
57 | :port => ENV['port'], |
|
92 | :port => ENV['port'], | |
58 | :ssl => ENV['ssl'], |
|
93 | :ssl => ENV['ssl'], | |
59 | :username => ENV['username'], |
|
94 | :username => ENV['username'], | |
60 | :password => ENV['password'], |
|
95 | :password => ENV['password'], | |
61 | :folder => ENV['folder']} |
|
96 | :folder => ENV['folder']} | |
62 |
|
97 | |||
63 | options = {} |
|
98 | options = { :issue => {} } | |
64 | options[:project] = ENV['project'] if ENV['project'] |
|
99 | %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } | |
|
100 | options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] | |||
65 |
|
101 | |||
66 | Redmine::IMAP.check(imap_options, options) |
|
102 | Redmine::IMAP.check(imap_options, options) | |
67 | end |
|
103 | end | |
68 | end |
|
104 | end | |
69 | end |
|
105 | end |
@@ -1,11 +1,17 | |||||
1 | --- |
|
1 | --- | |
2 | issue_categories_001: |
|
2 | issue_categories_001: | |
3 | name: Printing |
|
3 | name: Printing | |
4 | project_id: 1 |
|
4 | project_id: 1 | |
5 | assigned_to_id: 2 |
|
5 | assigned_to_id: 2 | |
6 | id: 1 |
|
6 | id: 1 | |
7 | issue_categories_002: |
|
7 | issue_categories_002: | |
8 | name: Recipes |
|
8 | name: Recipes | |
9 | project_id: 1 |
|
9 | project_id: 1 | |
10 | assigned_to_id: |
|
10 | assigned_to_id: | |
11 | id: 2 |
|
11 | id: 2 | |
|
12 | issue_categories_003: | |||
|
13 | name: Stock management | |||
|
14 | project_id: 2 | |||
|
15 | assigned_to_id: | |||
|
16 | id: 3 | |||
|
17 | No newline at end of file |
@@ -1,71 +1,107 | |||||
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, :enabled_modules, :roles, :members, :issues, :trackers, :enumerations |
|
21 | fixtures :users, :projects, | |
|
22 | :enabled_modules, | |||
|
23 | :roles, | |||
|
24 | :members, | |||
|
25 | :issues, | |||
|
26 | :trackers, | |||
|
27 | :projects_trackers, | |||
|
28 | :enumerations, | |||
|
29 | :issue_categories | |||
22 |
|
30 | |||
23 | FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' |
|
31 | FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' | |
24 |
|
32 | |||
25 | def setup |
|
33 | def setup | |
26 | ActionMailer::Base.deliveries.clear |
|
34 | ActionMailer::Base.deliveries.clear | |
27 | end |
|
35 | end | |
28 |
|
36 | |||
29 | def test_add_issue |
|
37 | def test_add_issue | |
30 | # This email contains: 'Project: onlinestore' |
|
38 | # This email contains: 'Project: onlinestore' | |
31 | issue = submit_email('ticket_on_given_project.eml') |
|
39 | issue = submit_email('ticket_on_given_project.eml') | |
32 | assert issue.is_a?(Issue) |
|
40 | assert issue.is_a?(Issue) | |
33 | assert !issue.new_record? |
|
41 | assert !issue.new_record? | |
34 | issue.reload |
|
42 | issue.reload | |
35 | assert_equal 'New ticket on a given project', issue.subject |
|
43 | assert_equal 'New ticket on a given project', issue.subject | |
36 | assert_equal User.find_by_login('jsmith'), issue.author |
|
44 | assert_equal User.find_by_login('jsmith'), issue.author | |
37 | assert_equal Project.find(2), issue.project |
|
45 | assert_equal Project.find(2), issue.project | |
38 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
46 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |
39 | end |
|
47 | end | |
40 |
|
48 | |||
|
49 | def test_add_issue_with_attributes_override | |||
|
50 | issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority') | |||
|
51 | assert issue.is_a?(Issue) | |||
|
52 | assert !issue.new_record? | |||
|
53 | issue.reload | |||
|
54 | assert_equal 'New ticket on a given project', issue.subject | |||
|
55 | assert_equal User.find_by_login('jsmith'), issue.author | |||
|
56 | assert_equal Project.find(2), issue.project | |||
|
57 | assert_equal 'Feature request', issue.tracker.to_s | |||
|
58 | assert_equal 'Stock management', issue.category.to_s | |||
|
59 | assert_equal 'Urgent', issue.priority.to_s | |||
|
60 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |||
|
61 | end | |||
|
62 | ||||
|
63 | def test_add_issue_with_partial_attributes_override | |||
|
64 | issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker']) | |||
|
65 | assert issue.is_a?(Issue) | |||
|
66 | assert !issue.new_record? | |||
|
67 | issue.reload | |||
|
68 | assert_equal 'New ticket on a given project', issue.subject | |||
|
69 | assert_equal User.find_by_login('jsmith'), issue.author | |||
|
70 | assert_equal Project.find(2), issue.project | |||
|
71 | assert_equal 'Feature request', issue.tracker.to_s | |||
|
72 | assert_nil issue.category | |||
|
73 | assert_equal 'High', issue.priority.to_s | |||
|
74 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |||
|
75 | end | |||
|
76 | ||||
41 | def test_add_issue_with_attachment_to_specific_project |
|
77 | def test_add_issue_with_attachment_to_specific_project | |
42 | issue = submit_email('ticket_with_attachment.eml', :project => 'onlinestore') |
|
78 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) | |
43 | assert issue.is_a?(Issue) |
|
79 | assert issue.is_a?(Issue) | |
44 | assert !issue.new_record? |
|
80 | assert !issue.new_record? | |
45 | issue.reload |
|
81 | issue.reload | |
46 | assert_equal 'Ticket created by email with attachment', issue.subject |
|
82 | assert_equal 'Ticket created by email with attachment', issue.subject | |
47 | assert_equal User.find_by_login('jsmith'), issue.author |
|
83 | assert_equal User.find_by_login('jsmith'), issue.author | |
48 | assert_equal Project.find(2), issue.project |
|
84 | assert_equal Project.find(2), issue.project | |
49 | assert_equal 'This is a new ticket with attachments', issue.description |
|
85 | assert_equal 'This is a new ticket with attachments', issue.description | |
50 | # Attachment properties |
|
86 | # Attachment properties | |
51 | assert_equal 1, issue.attachments.size |
|
87 | assert_equal 1, issue.attachments.size | |
52 | assert_equal 'Paella.jpg', issue.attachments.first.filename |
|
88 | assert_equal 'Paella.jpg', issue.attachments.first.filename | |
53 | assert_equal 'image/jpeg', issue.attachments.first.content_type |
|
89 | assert_equal 'image/jpeg', issue.attachments.first.content_type | |
54 | assert_equal 10790, issue.attachments.first.filesize |
|
90 | assert_equal 10790, issue.attachments.first.filesize | |
55 | end |
|
91 | end | |
56 |
|
92 | |||
57 | def test_add_issue_note |
|
93 | def test_add_issue_note | |
58 | journal = submit_email('ticket_reply.eml') |
|
94 | journal = submit_email('ticket_reply.eml') | |
59 | assert journal.is_a?(Journal) |
|
95 | assert journal.is_a?(Journal) | |
60 | assert_equal User.find_by_login('jsmith'), journal.user |
|
96 | assert_equal User.find_by_login('jsmith'), journal.user | |
61 | assert_equal Issue.find(2), journal.journalized |
|
97 | assert_equal Issue.find(2), journal.journalized | |
62 | assert_equal 'This is reply', journal.notes |
|
98 | assert_equal 'This is reply', journal.notes | |
63 | end |
|
99 | end | |
64 |
|
100 | |||
65 | private |
|
101 | private | |
66 |
|
102 | |||
67 | def submit_email(filename, options={}) |
|
103 | def submit_email(filename, options={}) | |
68 | raw = IO.read(File.join(FIXTURES_PATH, filename)) |
|
104 | raw = IO.read(File.join(FIXTURES_PATH, filename)) | |
69 | MailHandler.receive(raw, options) |
|
105 | MailHandler.receive(raw, options) | |
70 | end |
|
106 | end | |
71 | end |
|
107 | end |
General Comments 0
You need to be logged in to leave comments.
Login now