rdm-mailhandler (1).rb

with url and api key - Hugo Barnas, 2022-11-10 13:36

Download (10.1 KB)

 
1
#!/usr/bin/env ruby
2
#frozen_string_literal: false
3

    
4
# Redmine - project management software
5
# Copyright (C) 2006-2022  Jean-Philippe Lang
6
#
7
# This program is free software; you can redistribute it and/or
8
# modify it under the terms of the GNU General Public License
9
# as published by the Free Software Foundation; either version 2
10
# of the License, or (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program; if not, write to the Free Software
19
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20

    
21
require 'net/http'
22
require 'net/https'
23
require 'uri'
24
require 'optparse'
25

    
26
module Net
27
  class HTTPS < HTTP
28
    def self.post_form(url, params, headers, options={})
29
      request = Post.new(url.path)
30
      request.form_data = params
31
      request.initialize_http_header(headers)
32
      request.basic_auth url.user, url.password if url.user
33
      http = new(url.host, url.port)
34
      http.use_ssl = (url.scheme == 'https')
35
      if options[:certificate_bundle]
36
        http.ca_file = options[:certificate_bundle]
37
      end
38
      if options[:no_check_certificate]
39
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
40
      end
41
      http.start {|h| h.request(request) }
42
    end
43
  end
44
end
45

    
46
class RedmineMailHandler
47
  VERSION = '0.2.3'
48

    
49
  attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check,
50
    :url, :key, :no_check_certificate, :certificate_bundle, :no_account_notice, :no_notification, :project_from_subaddress
51

    
52
  def initialize
53
    self.issue_attributes = {}
54

    
55
    optparse = OptionParser.new do |opts|
56
      opts.banner = "Usage: rdm-mailhandler.rb [options] --url=https://redmine.immersive-display.com --key=2GpuhmkjwqtEl1U6DDfS"
57
      opts.separator("")
58
      opts.separator("Reads an email from standard input and forwards it to a Redmine server through a HTTP request.")
59
      opts.separator("")
60
      opts.separator("Required arguments:")
61
      opts.on("-u", "--url URL","URL of the Redmine server") {|v| self.url = v}
62
      opts.on("-k", "--key KEY","Redmine API key") {|v| self.key = v}
63
      opts.separator("")
64
      opts.separator("General options:")
65
      opts.on("--key-file FILE",              "full path to a file that contains your Redmine",
66
                                              "API key (use this option instead of --key if",
67
                                              "you don't want the key to appear in the command",
68
                                              "line)") {|v| read_key_from_file(v)}
69
      opts.on("--no-check-certificate",       "do not check server certificate") {self.no_check_certificate = true}
70
      opts.on("--certificate-bundle FILE",    "certificate bundle to use") {|v| self.certificate_bundle = v}
71
      opts.on("-h", "--help",                 "show this help") {puts opts; exit 1}
72
      opts.on("-v", "--verbose",              "show extra information") {self.verbose = true}
73
      opts.on("-V", "--version",              "show version information and exit") {puts VERSION; exit}
74
      opts.separator("")
75
      opts.separator("User and permissions options:")
76
      opts.on("--unknown-user ACTION",        "how to handle emails from an unknown user",
77
                                              "ACTION can be one of the following values:",
78
                                              "* ignore: email is ignored (default)",
79
                                              "* accept: accept as anonymous user",
80
                                              "* create: create a user account") {|v| self.unknown_user = v}
81
      opts.on("--no-permission-check",        "disable permission checking when receiving",
82
                                              "the email") {self.no_permission_check = '1'}
83
      opts.on("--default-group GROUP",        "add created user to GROUP (none by default)",
84
                                              "GROUP can be a comma separated list of groups") { |v| self.default_group = v}
85
      opts.on("--no-account-notice",          "don't send account information to the newly",
86
                                              "created user") { |v| self.no_account_notice = '1'}
87
      opts.on("--no-notification",            "disable email notifications for the created",
88
                                              "user") { |v| self.no_notification = '1'}
89
      opts.separator("")
90
      opts.separator("Issue attributes control options:")
91
      opts.on(      "--project-from-subaddress ADDR", "select project from subaddress of ADDR found",
92
                                              "in To, Cc, Bcc headers") {|v| self.project_from_subaddress = v}
93
      opts.on("-p", "--project PROJECT",      "identifier of the target project") {|v| self.issue_attributes['project'] = v}
94
      opts.on("-s", "--status STATUS",        "name of the target status") {|v| self.issue_attributes['status'] = v}
95
      opts.on("-t", "--tracker TRACKER",      "name of the target tracker") {|v| self.issue_attributes['tracker'] = v}
96
      opts.on(      "--category CATEGORY",    "name of the target category") {|v| self.issue_attributes['category'] = v}
97
      opts.on(      "--priority PRIORITY",    "name of the target priority") {|v| self.issue_attributes['priority'] = v}
98
      opts.on(      "--assigned-to ASSIGNEE", "assignee (username or group name)") {|v| self.issue_attributes['assigned_to'] = v}
99
      opts.on(      "--fixed-version VERSION","name of the target version") {|v| self.issue_attributes['fixed_version'] = v}
100
      opts.on(      "--private",              "create new issues as private") {|v| self.issue_attributes['is_private'] = '1'}
101
      opts.on("-o", "--allow-override ATTRS", "allow email content to set attributes values",
102
                                              "ATTRS is a comma separated list of attributes",
103
                                              "or 'all' to allow all attributes to be",
104
                                              "overridable (see below for details)") {|v| self.allow_override = v}
105

    
106
      opts.separator <<-END_DESC
107

108
Overrides:
109
  ATTRS is a comma separated list of attributes among:
110
  * project, tracker, status, priority, category, assigned_to, fixed_version,
111
    start_date, due_date, estimated_hours, done_ratio
112
  * custom fields names with underscores instead of spaces (case insensitive)
113
  Example: --allow-override=project,priority,my_custom_field
114

115
  If the --project option is not set, project is overridable by default for
116
  emails that create new issues.
117

118
  You can use --allow-override=all to allow all attributes to be overridable.
119

120
Examples:
121
  No project specified, emails MUST contain the 'Project' keyword, otherwise
122
  they will be dropped (not recommended):
123

124
    rdm-mailhandler.rb --url http://redmine.domain.foo --key secret
125

126
  Fixed project and default tracker specified, but emails can override
127
  both tracker and priority attributes using keywords:
128

129
    rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\
130
      --project myproject \\
131
      --tracker bug \\
132
      --allow-override tracker,priority
133

134
  Project selected by subaddress of redmine@example.net. Sending the email
135
  to redmine+myproject@example.net will add the issue to myproject:
136

137
    rdm-mailhandler.rb --url http://redmine.domain.foo --key secret \\
138
      --project-from-subaddress redmine@example.net
139
END_DESC
140

    
141
      opts.summary_width = 27
142
    end
143
    optparse.parse!
144

    
145
    unless url && key
146
      puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help."
147
      exit 1
148
    end
149
  end
150

    
151
  def submit(email)
152
    uri = url.gsub(%r{/*$}, '') + '/mail_handler'
153

    
154
    headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" }
155

    
156
    # MailHandlerController#index should permit all options set by
157
    # RedmineMailHandler#submit in rdm-mailhandler.rb.
158
    # It must be kept in sync.
159
    data = { 'key' => key, 'email' => email.gsub(/(?<!\r)\n|\r(?!\n)/, "\r\n"),
160
                           'allow_override' => allow_override,
161
                           'unknown_user' => unknown_user,
162
                           'default_group' => default_group,
163
                           'no_account_notice' => no_account_notice,
164
                           'no_notification' => no_notification,
165
                           'no_permission_check' => no_permission_check,
166
                           'project_from_subaddress' => project_from_subaddress}
167
    issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
168

    
169
    debug "Posting to #{uri}..."
170
    begin
171
      response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate, :certificate_bundle => certificate_bundle)
172
    rescue SystemCallError, IOError => e # connection refused, etc.
173
      warn "An error occurred while contacting your Redmine server: #{e.message}"
174
      return 75 # temporary failure
175
    end
176
    debug "Response received: #{response.code}"
177

    
178
    case response.code.to_i
179
      when 403
180
        warn "Request was denied by your Redmine server. " +
181
             "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
182
        return 77
183
      when 422
184
        warn "Request was denied by your Redmine server. " +
185
             "Possible reasons: email is sent from an invalid email address or is missing some information."
186
        return 77
187
      when 400..499
188
        warn "Request was denied by your Redmine server (#{response.code})."
189
        return 77
190
      when 500..599
191
        warn "Failed to contact your Redmine server (#{response.code})."
192
        return 75
193
      when 201
194
        debug "Processed successfully"
195
        return 0
196
      else
197
        return 1
198
    end
199
  end
200

    
201
  private
202

    
203
  def debug(msg)
204
    puts msg if verbose
205
  end
206

    
207
  def read_key_from_file(filename)
208
    begin
209
      self.key = File.read(filename).strip
210
    rescue => e
211
      $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}"
212
      exit 1
213
    end
214
  end
215
end
216

    
217
handler = RedmineMailHandler.new
218
exit(handler.submit(STDIN.read.force_encoding('ASCII-8BIT')))