probe.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. class EmailHelper
  3. class Probe
  4. #
  5. # Try to guess email channel configuration from basic user data.
  6. #
  7. def self.full(params)
  8. user, domain = EmailHelper.parse_email(params[:email])
  9. if !user || !domain
  10. return {
  11. result: 'invalid',
  12. messages: {
  13. email: "Invalid email '#{params[:email]}'."
  14. },
  15. }
  16. end
  17. # probe provider based settings
  18. provider_map = EmailHelper.provider(params[:email], params[:password])
  19. domains = [domain]
  20. # get mx records, try to find provider based on mx records
  21. mx_records = EmailHelper.mx_records(domain)
  22. domains.concat(mx_records)
  23. provider_map.each_value do |settings|
  24. domains.each do |domain_to_check|
  25. next if !domain_to_check.match?(%r{#{settings[:domain]}}i)
  26. # add folder to config if needed
  27. if params[:folder].present? && settings[:inbound] && settings[:inbound][:options]
  28. settings[:inbound][:options][:folder] = params[:folder]
  29. end
  30. config_set_verify_ssl(settings[:inbound], params)
  31. config_set_verify_ssl(settings[:outbound], params)
  32. # probe inbound
  33. Rails.logger.debug { "INBOUND PROBE PROVIDER: #{settings[:inbound].inspect}" }
  34. result_inbound = EmailHelper::Probe.inbound(settings[:inbound])
  35. Rails.logger.debug { "INBOUND RESULT PROVIDER: #{result_inbound.inspect}" }
  36. next if result_inbound[:result] != 'ok'
  37. # probe outbound
  38. Rails.logger.debug { "OUTBOUND PROBE PROVIDER: #{settings[:outbound].inspect}" }
  39. result_outbound = EmailHelper::Probe.outbound(settings[:outbound], params[:email])
  40. Rails.logger.debug { "OUTBOUND RESULT PROVIDER: #{result_outbound.inspect}" }
  41. next if result_outbound[:result] != 'ok'
  42. return {
  43. result: 'ok',
  44. content_messages: result_inbound[:content_messages],
  45. setting: settings,
  46. }
  47. end
  48. end
  49. # probe guess settings
  50. # probe inbound
  51. inbound_mx = EmailHelper.provider_inbound_mx(user, params[:email], params[:password], mx_records)
  52. inbound_guess = EmailHelper.provider_inbound_guess(user, params[:email], params[:password], domain)
  53. inbound_map = inbound_mx + inbound_guess
  54. result = {
  55. result: 'ok',
  56. setting: {}
  57. }
  58. success = false
  59. inbound_map.each do |config|
  60. # add folder to config if needed
  61. if params[:folder].present? && config[:options]
  62. config[:options][:folder] = params[:folder]
  63. end
  64. # Add SSL verification flag to configuration, if needed.
  65. config_set_verify_ssl(config, params)
  66. Rails.logger.debug { "INBOUND PROBE GUESS: #{config.inspect}" }
  67. result_inbound = EmailHelper::Probe.inbound(config)
  68. Rails.logger.debug { "INBOUND RESULT GUESS: #{result_inbound.inspect}" }
  69. next if result_inbound[:result] != 'ok'
  70. success = true
  71. result[:setting][:inbound] = config
  72. result[:content_messages] = result_inbound[:content_messages]
  73. break
  74. end
  75. # give up, no possible inbound found
  76. if !success
  77. return {
  78. result: 'failed',
  79. reason: 'inbound failed',
  80. }
  81. end
  82. # probe outbound
  83. outbound_mx = EmailHelper.provider_outbound_mx(user, params[:email], params[:password], mx_records)
  84. outbound_guess = EmailHelper.provider_outbound_guess(user, params[:email], params[:password], domain)
  85. outbound_map = outbound_mx + outbound_guess
  86. success = false
  87. outbound_map.each do |config|
  88. # Add SSL verification flag to configuration, if needed.
  89. config_set_verify_ssl(config, params)
  90. Rails.logger.debug { "OUTBOUND PROBE GUESS: #{config.inspect}" }
  91. result_outbound = EmailHelper::Probe.outbound(config, params[:email])
  92. Rails.logger.debug { "OUTBOUND RESULT GUESS: #{result_outbound.inspect}" }
  93. next if result_outbound[:result] != 'ok'
  94. success = true
  95. result[:setting][:outbound] = config
  96. break
  97. end
  98. # give up, no possible outbound found
  99. if !success
  100. return {
  101. result: 'failed',
  102. reason: 'outbound failed',
  103. }
  104. end
  105. Rails.logger.debug { "PROBE FULL SUCCESS: #{result.inspect}" }
  106. result
  107. end
  108. #
  109. # Validate an inbound email channel configuration.
  110. #
  111. def self.inbound(params)
  112. adapter = params[:adapter].downcase
  113. # validate adapter
  114. if !probeable_driver? adapter, :inbound
  115. return {
  116. result: 'failed',
  117. message: "Unknown adapter '#{adapter}'",
  118. }
  119. end
  120. # connection test
  121. result_inbound = {}
  122. begin
  123. driver_class = "Channel::Driver::#{adapter.to_classname}".constantize
  124. driver_instance = driver_class.new
  125. result_inbound = driver_instance.check_configuration(params[:options])
  126. rescue => e
  127. Rails.logger.debug { e }
  128. return {
  129. result: 'invalid',
  130. settings: params,
  131. message: e.message,
  132. message_human: translation(e.message),
  133. invalid_field: invalid_field(e.message),
  134. }
  135. end
  136. result_inbound
  137. end
  138. #
  139. # Validate an outbound email channel configuration.
  140. #
  141. def self.outbound(params, email, subject = nil)
  142. adapter = params[:adapter].downcase
  143. # validate adapter
  144. if !probeable_driver? adapter, :outbound
  145. return {
  146. result: 'failed',
  147. message: "Unknown adapter '#{adapter}'",
  148. }
  149. end
  150. # prepare test email
  151. # rubocop:disable Zammad/DetectTranslatableString
  152. mail = if subject
  153. {
  154. from: email,
  155. to: email,
  156. subject: "Zammad Getting started Test Email #{subject}",
  157. body: "This is a test email from Zammad to check if email sending and receiving work correctly.\n\nYou can ignore or delete this email.",
  158. }
  159. else
  160. {
  161. from: email,
  162. to: 'verify-external-smtp-sending@discard.zammad.org',
  163. subject: 'This is a Test Email',
  164. body: "This is a test email from Zammad to verify if Zammad can send emails to an external address.\n\nIf you see this email, you can ignore or delete it.",
  165. }
  166. end
  167. # rubocop:enable Zammad/DetectTranslatableString
  168. if subject.present?
  169. mail['X-Zammad-Test-Message'] = subject
  170. end
  171. mail['X-Zammad-Ignore'] = 'true'
  172. mail['X-Zammad-Fqdn'] = Setting.get('fqdn')
  173. mail['X-Zammad-Verify'] = 'true'
  174. mail['X-Zammad-Verify-Time'] = Time.zone.now.iso8601
  175. mail['X-Loop'] = 'yes'
  176. mail['Precedence'] = 'bulk'
  177. mail['Auto-Submitted'] = 'auto-generated'
  178. mail['X-Auto-Response-Suppress'] = 'All'
  179. # test connection
  180. begin
  181. driver_class = "Channel::Driver::#{adapter.to_classname}".constantize
  182. driver_instance = driver_class.new
  183. driver_instance.deliver(
  184. params[:options],
  185. mail,
  186. )
  187. rescue => e
  188. Rails.logger.debug { e }
  189. # check if sending email was ok, but mailserver rejected
  190. if !subject
  191. white_map = {
  192. 'Recipient address rejected' => true,
  193. 'Sender address rejected: Domain not found' => true,
  194. }
  195. white_map.each_key do |key|
  196. next if !e.message.match?(%r{#{Regexp.escape(key)}}i)
  197. return {
  198. result: 'ok',
  199. settings: params,
  200. notice: e.message,
  201. }
  202. end
  203. end
  204. return {
  205. result: 'invalid',
  206. settings: params,
  207. message: e.message,
  208. message_human: translation(e.message),
  209. invalid_field: invalid_field(e.message),
  210. }
  211. end
  212. {
  213. result: 'ok',
  214. }
  215. end
  216. def self.invalid_field(message_backend)
  217. invalid_fields.each do |key, fields|
  218. return fields if message_backend.match?(%r{#{Regexp.escape(key)}}i)
  219. end
  220. {}
  221. end
  222. def self.invalid_fields
  223. {
  224. 'authentication failed' => { user: true, password: true },
  225. 'Username and Password not accepted' => { user: true, password: true },
  226. 'Incorrect username' => { user: true, password: true },
  227. 'Lookup failed' => { user: true },
  228. 'Invalid credentials' => { user: true, password: true },
  229. 'getaddrinfo: nodename nor servname provided, or not known' => { host: true },
  230. 'getaddrinfo: Name or service not known' => { host: true },
  231. 'No route to host' => { host: true },
  232. 'execution expired' => { host: true },
  233. 'Connection refused' => { host: true },
  234. 'Mailbox doesn\'t exist' => { folder: true },
  235. 'Folder doesn\'t exist' => { folder: true },
  236. 'Unknown Mailbox' => { folder: true },
  237. }
  238. end
  239. def self.translation(message_backend)
  240. translations.each do |key, message_human|
  241. return message_human if message_backend.match?(%r{#{Regexp.escape(key)}}i)
  242. end
  243. nil
  244. end
  245. def self.translations
  246. {
  247. 'authentication failed' => __('Authentication failed.'),
  248. 'Username and Password not accepted' => __('Authentication failed.'),
  249. 'Incorrect username' => __('Authentication failed due to incorrect username.'),
  250. 'Lookup failed' => __('Authentication failed due to incorrect username.'),
  251. 'Invalid credentials' => __('Authentication failed due to incorrect credentials.'),
  252. 'authentication not enabled' => __('Authentication not possible (not offered by the service)'),
  253. 'getaddrinfo: nodename nor servname provided, or not known' => __('The hostname could not be found.'),
  254. 'getaddrinfo: Name or service not known' => __('The hostname could not be found.'),
  255. 'No route to host' => __('There is no route to this host.'),
  256. 'execution expired' => __('This host cannot be reached.'),
  257. 'Connection refused' => __('The connection was refused.'),
  258. }
  259. end
  260. def self.config_set_verify_ssl(config, params)
  261. return if !config[:options]
  262. if params.key?(:ssl_verify)
  263. config[:options][:ssl_verify] = params[:ssl_verify]
  264. elsif config[:options][:ssl] || config[:options][:start_tls]
  265. config[:options][:ssl_verify] = true
  266. else
  267. config[:options][:ssl_verify] ||= false
  268. end
  269. end
  270. # Returns all probeable drivers. Which are not limited to classic email drivers available for email setup.
  271. # Emailhelper.available_driver returns drivers available for generic email driver setup only.
  272. # So it needs to be extended with other drivers here
  273. def self.all_probeable_drivers
  274. EmailHelper
  275. .available_driver
  276. .tap do |elem|
  277. elem[:inbound][:microsoft_graph_inbound] = true
  278. elem[:outbound][:microsoft_graph_outbound] = true
  279. end
  280. end
  281. def self.probeable_driver?(name, direction)
  282. all_probeable_drivers[direction.to_sym].key? name.to_sym
  283. end
  284. end
  285. end