probe.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. class EmailHelper
  3. class Probe
  4. =begin
  5. get result of probe
  6. result = EmailHelper::Probe.full(
  7. email: 'zammad@example.com',
  8. password: 'somepassword',
  9. folder: 'some_folder', # optional im imap
  10. )
  11. returns on success
  12. {
  13. result: 'ok',
  14. settings: {
  15. inbound: {
  16. adapter: 'imap',
  17. options: {
  18. host: 'imap.gmail.com',
  19. port: 993,
  20. ssl: true,
  21. user: 'some@example.com',
  22. password: 'password',
  23. folder: 'some_folder', # optional im imap
  24. },
  25. },
  26. outbound: {
  27. adapter: 'smtp',
  28. options: {
  29. host: 'smtp.gmail.com',
  30. port: 25,
  31. ssl: true,
  32. user: 'some@example.com',
  33. password: 'password',
  34. },
  35. },
  36. }
  37. }
  38. returns on fail
  39. result = {
  40. result: 'failed',
  41. }
  42. =end
  43. def self.full(params)
  44. user, domain = EmailHelper.parse_email(params[:email])
  45. if !user || !domain
  46. return {
  47. result: 'invalid',
  48. messages: {
  49. email: "Invalid email '#{params[:email]}'."
  50. },
  51. }
  52. end
  53. # probe provider based settings
  54. provider_map = EmailHelper.provider(params[:email], params[:password])
  55. domains = [domain]
  56. # get mx records, try to find provider based on mx records
  57. mx_records = EmailHelper.mx_records(domain)
  58. domains.concat(mx_records)
  59. provider_map.each_value do |settings|
  60. domains.each do |domain_to_check|
  61. next if !domain_to_check.match?(%r{#{settings[:domain]}}i)
  62. # add folder to config if needed
  63. if params[:folder].present? && settings[:inbound] && settings[:inbound][:options]
  64. settings[:inbound][:options][:folder] = params[:folder]
  65. end
  66. # probe inbound
  67. Rails.logger.debug { "INBOUND PROBE PROVIDER: #{settings[:inbound].inspect}" }
  68. result_inbound = EmailHelper::Probe.inbound(settings[:inbound])
  69. Rails.logger.debug { "INBOUND RESULT PROVIDER: #{result_inbound.inspect}" }
  70. next if result_inbound[:result] != 'ok'
  71. # probe outbound
  72. Rails.logger.debug { "OUTBOUND PROBE PROVIDER: #{settings[:outbound].inspect}" }
  73. result_outbound = EmailHelper::Probe.outbound(settings[:outbound], params[:email])
  74. Rails.logger.debug { "OUTBOUND RESULT PROVIDER: #{result_outbound.inspect}" }
  75. next if result_outbound[:result] != 'ok'
  76. return {
  77. result: 'ok',
  78. content_messages: result_inbound[:content_messages],
  79. archive_possible: result_inbound[:archive_possible],
  80. archive_week_range: result_inbound[:archive_week_range],
  81. setting: settings,
  82. }
  83. end
  84. end
  85. # probe guess settings
  86. # probe inbound
  87. inbound_mx = EmailHelper.provider_inbound_mx(user, params[:email], params[:password], mx_records)
  88. inbound_guess = EmailHelper.provider_inbound_guess(user, params[:email], params[:password], domain)
  89. inbound_map = inbound_mx + inbound_guess
  90. result = {
  91. result: 'ok',
  92. setting: {}
  93. }
  94. success = false
  95. inbound_map.each do |config|
  96. # add folder to config if needed
  97. if params[:folder].present? && config[:options]
  98. config[:options][:folder] = params[:folder]
  99. end
  100. Rails.logger.debug { "INBOUND PROBE GUESS: #{config.inspect}" }
  101. result_inbound = EmailHelper::Probe.inbound(config)
  102. Rails.logger.debug { "INBOUND RESULT GUESS: #{result_inbound.inspect}" }
  103. next if result_inbound[:result] != 'ok'
  104. success = true
  105. result[:setting][:inbound] = config
  106. result[:content_messages] = result_inbound[:content_messages]
  107. result[:archive_possible] = result_inbound[:archive_possible]
  108. result[:archive_week_range] = result_inbound[:archive_week_range]
  109. break
  110. end
  111. # give up, no possible inbound found
  112. if !success
  113. return {
  114. result: 'failed',
  115. reason: 'inbound failed',
  116. }
  117. end
  118. # probe outbound
  119. outbound_mx = EmailHelper.provider_outbound_mx(user, params[:email], params[:password], mx_records)
  120. outbound_guess = EmailHelper.provider_outbound_guess(user, params[:email], params[:password], domain)
  121. outbound_map = outbound_mx + outbound_guess
  122. success = false
  123. outbound_map.each do |config|
  124. Rails.logger.debug { "OUTBOUND PROBE GUESS: #{config.inspect}" }
  125. result_outbound = EmailHelper::Probe.outbound(config, params[:email])
  126. Rails.logger.debug { "OUTBOUND RESULT GUESS: #{result_outbound.inspect}" }
  127. next if result_outbound[:result] != 'ok'
  128. success = true
  129. result[:setting][:outbound] = config
  130. break
  131. end
  132. # give up, no possible outbound found
  133. if !success
  134. return {
  135. result: 'failed',
  136. reason: 'outbound failed',
  137. }
  138. end
  139. Rails.logger.debug { "PROBE FULL SUCCESS: #{result.inspect}" }
  140. result
  141. end
  142. =begin
  143. get result of inbound probe
  144. result = EmailHelper::Probe.inbound(
  145. adapter: 'imap',
  146. options: {
  147. host: 'imap.gmail.com',
  148. port: 993,
  149. ssl: true,
  150. user: 'some@example.com',
  151. password: 'password',
  152. folder: 'some_folder', # optional
  153. }
  154. )
  155. returns on success
  156. {
  157. result: 'ok'
  158. }
  159. returns on fail
  160. result = {
  161. result: 'invalid',
  162. settings: {
  163. host: 'imap.gmail.com',
  164. port: 993,
  165. ssl: true,
  166. user: 'some@example.com',
  167. password: 'password',
  168. folder: 'some_folder', # optional im imap
  169. },
  170. message: 'error message from used lib',
  171. message_human: 'translated error message, readable for humans',
  172. }
  173. =end
  174. def self.inbound(params)
  175. adapter = params[:adapter].downcase
  176. # validate adapter
  177. if !EmailHelper.available_driver[:inbound][adapter.to_sym]
  178. return {
  179. result: 'failed',
  180. message: "Unknown adapter '#{adapter}'",
  181. }
  182. end
  183. # connection test
  184. result_inbound = {}
  185. begin
  186. driver_class = "Channel::Driver::#{adapter.to_classname}".constantize
  187. driver_instance = driver_class.new
  188. result_inbound = driver_instance.fetch(params[:options], nil, 'check')
  189. rescue => e
  190. Rails.logger.debug { e }
  191. return {
  192. result: 'invalid',
  193. settings: params,
  194. message: e.message,
  195. message_human: translation(e.message),
  196. invalid_field: invalid_field(e.message),
  197. }
  198. end
  199. result_inbound
  200. end
  201. =begin
  202. get result of outbound probe
  203. result = EmailHelper::Probe.outbound(
  204. {
  205. adapter: 'smtp',
  206. options: {
  207. host: 'smtp.gmail.com',
  208. port: 25,
  209. ssl: true,
  210. user: 'some@example.com',
  211. password: 'password',
  212. }
  213. },
  214. 'sender_and_recipient_of_test_email@example.com',
  215. 'subject of probe email',
  216. )
  217. returns on success
  218. {
  219. result: 'ok'
  220. }
  221. returns on fail
  222. result = {
  223. result: 'invalid',
  224. settings: {
  225. host: 'stmp.gmail.com',
  226. port: 25,
  227. ssl: true,
  228. user: 'some@example.com',
  229. password: 'password',
  230. },
  231. message: 'error message from used lib',
  232. message_human: 'translated error message, readable for humans',
  233. }
  234. =end
  235. def self.outbound(params, email, subject = nil)
  236. adapter = params[:adapter].downcase
  237. # validate adapter
  238. if !EmailHelper.available_driver[:outbound][adapter.to_sym]
  239. return {
  240. result: 'failed',
  241. message: "Unknown adapter '#{adapter}'",
  242. }
  243. end
  244. # prepare test email
  245. # rubocop:disable Zammad/DetectTranslatableString
  246. mail = if subject
  247. {
  248. from: email,
  249. to: email,
  250. subject: "Zammad Getting started Test Email #{subject}",
  251. 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.",
  252. }
  253. else
  254. {
  255. from: email,
  256. to: 'verify-external-smtp-sending@discard.zammad.org',
  257. subject: 'This is a Test Email',
  258. 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.",
  259. }
  260. end
  261. # rubocop:enable Zammad/DetectTranslatableString
  262. if subject.present?
  263. mail['X-Zammad-Test-Message'] = subject
  264. end
  265. mail['X-Zammad-Ignore'] = 'true'
  266. mail['X-Zammad-Fqdn'] = Setting.get('fqdn')
  267. mail['X-Zammad-Verify'] = 'true'
  268. mail['X-Zammad-Verify-Time'] = Time.zone.now.iso8601
  269. mail['X-Loop'] = 'yes'
  270. mail['Precedence'] = 'bulk'
  271. mail['Auto-Submitted'] = 'auto-generated'
  272. mail['X-Auto-Response-Suppress'] = 'All'
  273. # test connection
  274. begin
  275. driver_class = "Channel::Driver::#{adapter.to_classname}".constantize
  276. driver_instance = driver_class.new
  277. driver_instance.send(
  278. params[:options],
  279. mail,
  280. )
  281. rescue => e
  282. Rails.logger.debug { e }
  283. # check if sending email was ok, but mailserver rejected
  284. if !subject
  285. white_map = {
  286. 'Recipient address rejected' => true,
  287. 'Sender address rejected: Domain not found' => true,
  288. }
  289. white_map.each_key do |key|
  290. next if !e.message.match?(%r{#{Regexp.escape(key)}}i)
  291. return {
  292. result: 'ok',
  293. settings: params,
  294. notice: e.message,
  295. }
  296. end
  297. end
  298. return {
  299. result: 'invalid',
  300. settings: params,
  301. message: e.message,
  302. message_human: translation(e.message),
  303. invalid_field: invalid_field(e.message),
  304. }
  305. end
  306. {
  307. result: 'ok',
  308. }
  309. end
  310. def self.invalid_field(message_backend)
  311. invalid_fields.each do |key, fields|
  312. return fields if message_backend.match?(%r{#{Regexp.escape(key)}}i)
  313. end
  314. {}
  315. end
  316. def self.invalid_fields
  317. {
  318. 'authentication failed' => { user: true, password: true },
  319. 'Username and Password not accepted' => { user: true, password: true },
  320. 'Incorrect username' => { user: true, password: true },
  321. 'Lookup failed' => { user: true },
  322. 'Invalid credentials' => { user: true, password: true },
  323. 'getaddrinfo: nodename nor servname provided, or not known' => { host: true },
  324. 'getaddrinfo: Name or service not known' => { host: true },
  325. 'No route to host' => { host: true },
  326. 'execution expired' => { host: true },
  327. 'Connection refused' => { host: true },
  328. 'Mailbox doesn\'t exist' => { folder: true },
  329. 'Folder doesn\'t exist' => { folder: true },
  330. 'Unknown Mailbox' => { folder: true },
  331. }
  332. end
  333. def self.translation(message_backend)
  334. translations.each do |key, message_human|
  335. return message_human if message_backend.match?(%r{#{Regexp.escape(key)}}i)
  336. end
  337. nil
  338. end
  339. def self.translations
  340. {
  341. 'authentication failed' => __('Authentication failed.'),
  342. 'Username and Password not accepted' => __('Authentication failed.'),
  343. 'Incorrect username' => __('Authentication failed due to incorrect username.'),
  344. 'Lookup failed' => __('Authentication failed due to incorrect username.'),
  345. 'Invalid credentials' => __('Authentication failed due to incorrect credentials.'),
  346. 'authentication not enabled' => __('Authentication not possible (not offered by the service)'),
  347. 'getaddrinfo: nodename nor servname provided, or not known' => __('The hostname could not be found.'),
  348. 'getaddrinfo: Name or service not known' => __('The hostname could not be found.'),
  349. 'No route to host' => __('There is no route to this host.'),
  350. 'execution expired' => __('This host cannot be reached.'),
  351. 'Connection refused' => __('The connection was refused.'),
  352. }
  353. end
  354. end
  355. end