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 9.9KB

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