You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

rdm-mailhandler.rb 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. #!/usr/bin/env ruby
  2. # frozen_string_literal: false
  3. # Redmine - project management software
  4. # Copyright (C) 2006-2022 Jean-Philippe Lang
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License
  8. # as published by the Free Software Foundation; either version 2
  9. # of the License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. require 'net/http'
  20. require 'net/https'
  21. require 'uri'
  22. require 'optparse'
  23. module Net
  24. class HTTPS < HTTP
  25. def self.post_form(url, params, headers, options={})
  26. request = Post.new(url.path)
  27. request.form_data = params
  28. request.initialize_http_header(headers)
  29. request.basic_auth url.user, url.password if url.user
  30. http = new(url.host, url.port)
  31. http.use_ssl = (url.scheme == 'https')
  32. if options[:certificate_bundle]
  33. http.ca_file = options[:certificate_bundle]
  34. end
  35. if options[:no_check_certificate]
  36. http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  37. end
  38. http.start {|h| h.request(request) }
  39. end
  40. end
  41. end
  42. class RedmineMailHandler
  43. VERSION = '0.2.3'
  44. attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check,
  45. :url, :key, :no_check_certificate, :certificate_bundle, :no_account_notice, :no_notification, :project_from_subaddress
  46. def initialize
  47. self.issue_attributes = {}
  48. optparse = OptionParser.new do |opts|
  49. opts.banner = "Usage: rdm-mailhandler.rb [options] --url=<Redmine URL> --key=<API key>"
  50. opts.separator("")
  51. opts.separator("Reads an email from standard input and forwards it to a Redmine server through a HTTP request.")
  52. opts.separator("")
  53. opts.separator("Required arguments:")
  54. opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v}
  55. opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v}
  56. opts.separator("")
  57. opts.separator("General options:")
  58. opts.on("--key-file FILE", "full path to a file that contains your Redmine",
  59. "API key (use this option instead of --key if",
  60. "you don't want the key to appear in the command",
  61. "line)") {|v| read_key_from_file(v)}
  62. opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true}
  63. opts.on("--certificate-bundle FILE", "certificate bundle to use") {|v| self.certificate_bundle = v}
  64. opts.on("-h", "--help", "show this help") {puts opts; exit 1}
  65. opts.on("-v", "--verbose", "show extra information") {self.verbose = true}
  66. opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit}
  67. opts.separator("")
  68. opts.separator("User and permissions options:")
  69. opts.on("--unknown-user ACTION", "how to handle emails from an unknown user",
  70. "ACTION can be one of the following values:",
  71. "* ignore: email is ignored (default)",
  72. "* accept: accept as anonymous user",
  73. "* create: create a user account") {|v| self.unknown_user = v}
  74. opts.on("--no-permission-check", "disable permission checking when receiving",
  75. "the email") {self.no_permission_check = '1'}
  76. opts.on("--default-group GROUP", "add created user to GROUP (none by default)",
  77. "GROUP can be a comma separated list of groups") { |v| self.default_group = v}
  78. opts.on("--no-account-notice", "don't send account information to the newly",
  79. "created user") { |v| self.no_account_notice = '1'}
  80. opts.on("--no-notification", "disable email notifications for the created",
  81. "user") { |v| self.no_notification = '1'}
  82. opts.separator("")
  83. opts.separator("Issue attributes control options:")
  84. opts.on( "--project-from-subaddress ADDR", "select project from subaddress of ADDR found",
  85. "in To, Cc, Bcc headers") {|v| self.project_from_subaddress = v}
  86. opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v}
  87. opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v}
  88. opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v}
  89. opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v}
  90. opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v}
  91. opts.on( "--assigned-to ASSIGNEE", "assignee (username or group name)") {|v| self.issue_attributes['assigned_to'] = v}
  92. opts.on( "--fixed-version VERSION","name of the target version") {|v| self.issue_attributes['fixed_version'] = v}
  93. opts.on( "--private", "create new issues as private") {|v| self.issue_attributes['is_private'] = '1'}
  94. opts.on("-o", "--allow-override ATTRS", "allow email content to set attributes values",
  95. "ATTRS is a comma separated list of attributes",
  96. "or 'all' to allow all attributes to be",
  97. "overridable (see below for details)") {|v| self.allow_override = v}
  98. opts.separator <<-END_DESC
  99. Overrides:
  100. ATTRS is a comma separated list of attributes among:
  101. * project, tracker, status, priority, category, assigned_to, fixed_version,
  102. start_date, due_date, estimated_hours, done_ratio
  103. * custom fields names with underscores instead of spaces (case insensitive)
  104. Example: --allow-override=project,priority,my_custom_field
  105. If the --project option is not set, project is overridable by default for
  106. emails that create new issues.
  107. You can use --allow-override=all to allow all attributes to be overridable.
  108. Examples:
  109. No project specified, emails MUST contain the 'Project' keyword, otherwise
  110. they will be dropped (not recommended):
  111. rdm-mailhandler.rb --url http://redmine.domain.foo --key secret
  112. Fixed project and default tracker specified, but emails can override
  113. both tracker and priority attributes using keywords:
  114. rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\
  115. --project myproject \\
  116. --tracker bug \\
  117. --allow-override tracker,priority
  118. Project selected by subaddress of redmine@example.net. Sending the email
  119. to redmine+myproject@example.net will add the issue to myproject:
  120. rdm-mailhandler.rb --url http://redmine.domain.foo --key secret \\
  121. --project-from-subaddress redmine@example.net
  122. END_DESC
  123. opts.summary_width = 27
  124. end
  125. optparse.parse!
  126. unless url && key
  127. puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help."
  128. exit 1
  129. end
  130. end
  131. def submit(email)
  132. uri = url.gsub(%r{/*$}, '') + '/mail_handler'
  133. headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" }
  134. # MailHandlerController#index should permit all options set by
  135. # RedmineMailHandler#submit in rdm-mailhandler.rb.
  136. # It must be kept in sync.
  137. data = { 'key' => key, 'email' => email.gsub(/(?<!\r)\n|\r(?!\n)/, "\r\n"),
  138. 'allow_override' => allow_override,
  139. 'unknown_user' => unknown_user,
  140. 'default_group' => default_group,
  141. 'no_account_notice' => no_account_notice,
  142. 'no_notification' => no_notification,
  143. 'no_permission_check' => no_permission_check,
  144. 'project_from_subaddress' => project_from_subaddress}
  145. issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
  146. debug "Posting to #{uri}..."
  147. begin
  148. response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate, :certificate_bundle => certificate_bundle)
  149. rescue SystemCallError, IOError => e # connection refused, etc.
  150. warn "An error occurred while contacting your Redmine server: #{e.message}"
  151. return 75 # temporary failure
  152. end
  153. debug "Response received: #{response.code}"
  154. case response.code.to_i
  155. when 403
  156. warn "Request was denied by your Redmine server. " +
  157. "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
  158. return 77
  159. when 422
  160. warn "Request was denied by your Redmine server. " +
  161. "Possible reasons: email is sent from an invalid email address or is missing some information."
  162. return 77
  163. when 400..499
  164. warn "Request was denied by your Redmine server (#{response.code})."
  165. return 77
  166. when 500..599
  167. warn "Failed to contact your Redmine server (#{response.code})."
  168. return 75
  169. when 201
  170. debug "Processed successfully"
  171. return 0
  172. else
  173. return 1
  174. end
  175. end
  176. private
  177. def debug(msg)
  178. puts msg if verbose
  179. end
  180. def read_key_from_file(filename)
  181. begin
  182. self.key = File.read(filename).strip
  183. rescue => e
  184. $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}"
  185. exit 1
  186. end
  187. end
  188. end
  189. handler = RedmineMailHandler.new
  190. exit(handler.submit(STDIN.read.force_encoding('ASCII-8BIT')))