probe.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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.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. archive_possible: result_inbound[:archive_possible],
  79. archive_week_range: result_inbound[:archive_week_range],
  80. setting: settings,
  81. }
  82. end
  83. end
  84. # probe guess settings
  85. # probe inbound
  86. inbound_mx = EmailHelper.provider_inbound_mx(user, params[:email], params[:password], mx_records)
  87. inbound_guess = EmailHelper.provider_inbound_guess(user, params[:email], params[:password], domain)
  88. inbound_map = inbound_mx + inbound_guess
  89. result = {
  90. result: 'ok',
  91. setting: {}
  92. }
  93. success = false
  94. inbound_map.each do |config|
  95. # add folder to config if needed
  96. if params[:folder].present? && config[:options]
  97. config[:options][:folder] = params[:folder]
  98. end
  99. Rails.logger.debug { "INBOUND PROBE GUESS: #{config.inspect}" }
  100. result_inbound = EmailHelper::Probe.inbound(config)
  101. Rails.logger.debug { "INBOUND RESULT GUESS: #{result_inbound.inspect}" }
  102. next if result_inbound[:result] != 'ok'
  103. success = true
  104. result[:setting][:inbound] = config
  105. result[:content_messages] = result_inbound[:content_messages]
  106. result[:archive_possible] = result_inbound[:archive_possible]
  107. result[:archive_week_range] = result_inbound[:archive_week_range]
  108. break
  109. end
  110. # give up, no possible inbound found
  111. if !success
  112. return {
  113. result: 'failed',
  114. reason: 'inbound failed',
  115. }
  116. end
  117. # probe outbound
  118. outbound_mx = EmailHelper.provider_outbound_mx(user, params[:email], params[:password], mx_records)
  119. outbound_guess = EmailHelper.provider_outbound_guess(user, params[:email], params[:password], domain)
  120. outbound_map = outbound_mx + outbound_guess
  121. success = false
  122. outbound_map.each do |config|
  123. Rails.logger.debug { "OUTBOUND PROBE GUESS: #{config.inspect}" }
  124. result_outbound = EmailHelper::Probe.outbound(config, params[:email])
  125. Rails.logger.debug { "OUTBOUND RESULT GUESS: #{result_outbound.inspect}" }
  126. next if result_outbound[:result] != 'ok'
  127. success = true
  128. result[:setting][:outbound] = config
  129. break
  130. end
  131. # give up, no possible outbound found
  132. if !success
  133. return {
  134. result: 'failed',
  135. reason: 'outbound failed',
  136. }
  137. end
  138. Rails.logger.info "PROBE FULL SUCCESS: #{result.inspect}"
  139. result
  140. end
  141. =begin
  142. get result of inbound probe
  143. result = EmailHelper::Probe.inbound(
  144. adapter: 'imap',
  145. options: {
  146. host: 'imap.gmail.com',
  147. port: 993,
  148. ssl: true,
  149. user: 'some@example.com',
  150. password: 'password',
  151. folder: 'some_folder', # optional
  152. }
  153. )
  154. returns on success
  155. {
  156. result: 'ok'
  157. }
  158. returns on fail
  159. result = {
  160. result: 'invalid',
  161. settings: {
  162. host: 'imap.gmail.com',
  163. port: 993,
  164. ssl: true,
  165. user: 'some@example.com',
  166. password: 'password',
  167. folder: 'some_folder', # optional im imap
  168. },
  169. message: 'error message from used lib',
  170. message_human: 'translated error message, readable for humans',
  171. }
  172. =end
  173. def self.inbound(params)
  174. adapter = params[:adapter].downcase
  175. # validate adapter
  176. if !EmailHelper.available_driver[:inbound][adapter.to_sym]
  177. return {
  178. result: 'failed',
  179. message: "Unknown adapter '#{adapter}'",
  180. }
  181. end
  182. # connection test
  183. result_inbound = {}
  184. begin
  185. require_dependency "channel/driver/#{adapter.to_filename}"
  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. mail = if subject
  246. {
  247. from: email,
  248. to: email,
  249. subject: "Zammad Getting started Test Email #{subject}",
  250. 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.",
  251. }
  252. else
  253. {
  254. from: email,
  255. to: 'emailtrytest@znuny.com',
  256. subject: 'This is a Test Email',
  257. 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.",
  258. }
  259. end
  260. if subject.present?
  261. mail['X-Zammad-Test-Message'] = subject
  262. end
  263. mail['X-Zammad-Ignore'] = 'true'
  264. mail['X-Zammad-Fqdn'] = Setting.get('fqdn')
  265. mail['X-Zammad-Verify'] = 'true'
  266. mail['X-Zammad-Verify-Time'] = Time.zone.now.iso8601
  267. mail['X-Loop'] = 'yes'
  268. mail['Precedence'] = 'bulk'
  269. mail['Auto-Submitted'] = 'auto-generated'
  270. mail['X-Auto-Response-Suppress'] = 'All'
  271. # test connection
  272. begin
  273. require_dependency "channel/driver/#{adapter.to_filename}"
  274. driver_class = "Channel::Driver::#{adapter.to_classname}".constantize
  275. driver_instance = driver_class.new
  276. driver_instance.send(
  277. params[:options],
  278. mail,
  279. )
  280. rescue => e
  281. Rails.logger.debug { e }
  282. # check if sending email was ok, but mailserver rejected
  283. if !subject
  284. white_map = {
  285. 'Recipient address rejected' => true,
  286. 'Sender address rejected: Domain not found' => true,
  287. }
  288. white_map.each_key do |key|
  289. next if !e.message.match?(/#{Regexp.escape(key)}/i)
  290. return {
  291. result: 'ok',
  292. settings: params,
  293. notice: e.message,
  294. }
  295. end
  296. end
  297. return {
  298. result: 'invalid',
  299. settings: params,
  300. message: e.message,
  301. message_human: translation(e.message),
  302. invalid_field: invalid_field(e.message),
  303. }
  304. end
  305. {
  306. result: 'ok',
  307. }
  308. end
  309. def self.invalid_field(message_backend)
  310. invalid_fields.each do |key, fields|
  311. return fields if message_backend.match?(/#{Regexp.escape(key)}/i)
  312. end
  313. {}
  314. end
  315. def self.invalid_fields
  316. {
  317. 'authentication failed' => { user: true, password: true },
  318. 'Username and Password not accepted' => { user: true, password: true },
  319. 'Incorrect username' => { user: true, password: true },
  320. 'Lookup failed' => { user: true },
  321. 'Invalid credentials' => { user: true, password: true },
  322. 'getaddrinfo: nodename nor servname provided, or not known' => { host: true },
  323. 'getaddrinfo: Name or service not known' => { host: true },
  324. 'No route to host' => { host: true },
  325. 'execution expired' => { host: true },
  326. 'Connection refused' => { host: true },
  327. 'Mailbox doesn\'t exist' => { folder: true },
  328. 'Folder doesn\'t exist' => { folder: true },
  329. 'Unknown Mailbox' => { folder: true },
  330. }
  331. end
  332. def self.translation(message_backend)
  333. translations.each do |key, message_human|
  334. return message_human if message_backend.match?(/#{Regexp.escape(key)}/i)
  335. end
  336. nil
  337. end
  338. def self.translations
  339. {
  340. 'authentication failed' => 'Authentication failed!',
  341. 'Username and Password not accepted' => 'Authentication failed!',
  342. 'Incorrect username' => 'Authentication failed, username incorrect!',
  343. 'Lookup failed' => 'Authentication failed, username incorrect!',
  344. 'Invalid credentials' => 'Authentication failed, invalid credentials!',
  345. 'authentication not enabled' => 'Authentication not possible (not offered by the service)',
  346. 'getaddrinfo: nodename nor servname provided, or not known' => 'Hostname not found!',
  347. 'getaddrinfo: Name or service not known' => 'Hostname not found!',
  348. 'No route to host' => 'No route to host!',
  349. 'execution expired' => 'Host not reachable!',
  350. 'Connection refused' => 'Connection refused!',
  351. }
  352. end
  353. end
  354. end