probe.rb 12 KB

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