imap.rb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
  2. require 'net/imap'
  3. class Channel::Driver::Imap < Channel::EmailParser
  4. FETCH_METADATA_TIMEOUT = 2.minutes
  5. FETCH_MSG_TIMEOUT = 4.minutes
  6. EXPUNGE_TIMEOUT = 16.minutes
  7. def fetchable?(_channel)
  8. true
  9. end
  10. =begin
  11. fetch emails from IMAP account
  12. instance = Channel::Driver::Imap.new
  13. result = instance.fetch(params[:inbound][:options], channel, 'verify', subject_looking_for)
  14. returns
  15. {
  16. result: 'ok',
  17. fetched: 123,
  18. notice: 'e. g. message about to big emails in mailbox',
  19. }
  20. check if connect to IMAP account is possible, return count of mails in mailbox
  21. instance = Channel::Driver::Imap.new
  22. result = instance.fetch(params[:inbound][:options], channel, 'check')
  23. returns
  24. {
  25. result: 'ok',
  26. content_messages: 123,
  27. }
  28. verify IMAP account, check if search email is in there
  29. instance = Channel::Driver::Imap.new
  30. result = instance.fetch(params[:inbound][:options], channel, 'verify', subject_looking_for)
  31. returns
  32. {
  33. result: 'ok', # 'verify not ok'
  34. }
  35. example
  36. params = {
  37. host: 'outlook.office365.com',
  38. user: 'xxx@znuny.onmicrosoft.com',
  39. password: 'xxx',
  40. keep_on_server: true,
  41. }
  42. OR
  43. params = {
  44. host: 'imap.gmail.com',
  45. user: 'xxx@gmail.com',
  46. password: 'xxx',
  47. keep_on_server: true,
  48. auth_type: 'XOAUTH2'
  49. }
  50. channel = Channel.last
  51. instance = Channel::Driver::Imap.new
  52. result = instance.fetch(params, channel, 'verify')
  53. =end
  54. def fetch(options, channel, check_type = '', verify_string = '')
  55. ssl = true
  56. starttls = false
  57. port = 993
  58. keep_on_server = false
  59. folder = 'INBOX'
  60. if options[:keep_on_server] == true || options[:keep_on_server] == 'true'
  61. keep_on_server = true
  62. end
  63. if options.key?(:ssl) && options[:ssl] == false
  64. ssl = false
  65. port = 143
  66. end
  67. port = if options.key?(:port) && options[:port].present?
  68. options[:port].to_i
  69. elsif ssl == true
  70. 993
  71. else
  72. 143
  73. end
  74. if ssl == true && port != 993
  75. ssl = false
  76. starttls = true
  77. end
  78. if options[:folder].present?
  79. folder = options[:folder]
  80. end
  81. Rails.logger.info "fetching imap (#{options[:host]}/#{options[:user]} port=#{port},ssl=#{ssl},starttls=#{starttls},folder=#{folder},keep_on_server=#{keep_on_server},auth_type=#{options.fetch(:auth_type, 'LOGIN')})"
  82. # on check, reduce open_timeout to have faster probing
  83. check_type_timeout = 45
  84. if check_type == 'check'
  85. check_type_timeout = 6
  86. end
  87. timeout(check_type_timeout) do
  88. @imap = ::Net::IMAP.new(options[:host], port, ssl, nil, false)
  89. if starttls
  90. @imap.starttls()
  91. end
  92. end
  93. timeout(check_type_timeout) do
  94. if options[:auth_type].present?
  95. @imap.authenticate(options[:auth_type], options[:user], options[:password])
  96. else
  97. @imap.login(options[:user], options[:password].dup&.force_encoding('ascii-8bit'))
  98. end
  99. end
  100. timeout(check_type_timeout) do
  101. # select folder
  102. @imap.select(folder)
  103. end
  104. # sort messages by date on server (if not supported), if not fetch messages via search (first in, first out)
  105. filter = ['ALL']
  106. if keep_on_server && check_type != 'check' && check_type != 'verify'
  107. filter = %w[NOT SEEN]
  108. end
  109. message_ids = nil
  110. timeout(6.minutes) do
  111. message_ids = @imap.sort(['DATE'], filter, 'US-ASCII')
  112. rescue
  113. message_ids = @imap.search(filter)
  114. end
  115. # check mode only
  116. if check_type == 'check'
  117. Rails.logger.info 'check only mode, fetch no emails'
  118. content_max_check = 2
  119. content_messages = 0
  120. # check messages
  121. message_ids.each do |message_id|
  122. message_meta = nil
  123. timeout(1.minute) do
  124. message_meta = @imap.fetch(message_id, ['RFC822.HEADER'])[0].attr
  125. end
  126. # check how many content messages we have, for notice used
  127. headers = parse_headers(message_meta['RFC822.HEADER'])
  128. next if messages_is_verify_message?(headers)
  129. next if messages_is_ignore_message?(headers)
  130. content_messages += 1
  131. break if content_max_check < content_messages
  132. end
  133. if content_messages >= content_max_check
  134. content_messages = message_ids.count
  135. end
  136. disconnect
  137. return {
  138. result: 'ok',
  139. content_messages: content_messages,
  140. }
  141. end
  142. # reverse message order to increase performance
  143. if check_type == 'verify'
  144. Rails.logger.info "verify mode, fetch no emails #{verify_string}"
  145. message_ids.reverse!
  146. # check for verify message
  147. message_ids.each do |message_id|
  148. message_meta = nil
  149. timeout(FETCH_METADATA_TIMEOUT) do
  150. message_meta = @imap.fetch(message_id, ['ENVELOPE'])[0].attr
  151. end
  152. # check if verify message exists
  153. subject = message_meta['ENVELOPE'].subject
  154. next if !subject
  155. next if !subject.match?(/#{verify_string}/)
  156. Rails.logger.info " - verify email #{verify_string} found"
  157. timeout(600) do
  158. @imap.store(message_id, '+FLAGS', [:Deleted])
  159. @imap.expunge()
  160. end
  161. disconnect
  162. return {
  163. result: 'ok',
  164. }
  165. end
  166. disconnect
  167. return {
  168. result: 'verify not ok',
  169. }
  170. end
  171. # fetch regular messages
  172. count_all = message_ids.count
  173. count = 0
  174. count_fetched = 0
  175. count_max = 5000
  176. too_large_messages = []
  177. active_check_interval = 20
  178. result = 'ok'
  179. notice = ''
  180. message_ids.each do |message_id|
  181. count += 1
  182. if (count % active_check_interval).zero?
  183. break if channel_has_changed?(channel)
  184. end
  185. break if max_process_count_has_reached?(channel, count, count_max)
  186. Rails.logger.info " - message #{count}/#{count_all}"
  187. message_meta = nil
  188. timeout(FETCH_METADATA_TIMEOUT) do
  189. message_meta = @imap.fetch(message_id, ['RFC822.SIZE', 'ENVELOPE', 'FLAGS', 'INTERNALDATE', 'RFC822.HEADER'])[0]
  190. rescue Net::IMAP::ResponseParseError => e
  191. raise if !e.message.include?('unknown token')
  192. result = 'error'
  193. notice += <<~NOTICE
  194. One of your incoming emails could not be imported (#{e.message}).
  195. Please remove it from your inbox directly
  196. to prevent Zammad from trying to import it again.
  197. NOTICE
  198. Rails.logger.error "Net::IMAP failed to parse message #{message_id}: #{e.message} (#{e.class})"
  199. Rails.logger.error '(See https://github.com/zammad/zammad/issues/2754 for more details)'
  200. end
  201. next if message_meta.nil?
  202. # ignore verify messages
  203. next if !messages_is_too_old_verify?(message_meta, count, count_all)
  204. # ignore deleted messages
  205. next if deleted?(message_meta, count, count_all)
  206. # ignore already imported
  207. next if already_imported?(message_id, message_meta, count, count_all, keep_on_server, channel)
  208. # delete email from server after article was created
  209. msg = nil
  210. begin
  211. timeout(FETCH_MSG_TIMEOUT) do
  212. msg = @imap.fetch(message_id, 'RFC822')[0].attr['RFC822']
  213. end
  214. rescue Timeout::Error => e
  215. Rails.logger.error "Unable to fetch email from #{count}/#{count_all} from server (#{options[:host]}/#{options[:user]}): #{e.inspect}"
  216. raise e
  217. end
  218. next if !msg
  219. # do not process too big messages, instead download & send postmaster reply
  220. too_large_info = too_large?(message_meta)
  221. if too_large_info
  222. if Setting.get('postmaster_send_reject_if_mail_too_large') == true
  223. info = " - download message #{count}/#{count_all} - ignore message because it's too large (is:#{too_large_info[0]} MB/max:#{too_large_info[1]} MB)"
  224. Rails.logger.info info
  225. notice += "#{info}\n"
  226. process_oversized_mail(channel, msg)
  227. else
  228. info = " - ignore message #{count}/#{count_all} - because message is too large (is:#{too_large_info[0]} MB/max:#{too_large_info[1]} MB)"
  229. Rails.logger.info info
  230. notice += "#{info}\n"
  231. too_large_messages.push info
  232. next
  233. end
  234. else
  235. process(channel, msg, false)
  236. end
  237. begin
  238. timeout(FETCH_MSG_TIMEOUT) do
  239. if !keep_on_server
  240. @imap.store(message_id, '+FLAGS', [:Deleted])
  241. else
  242. @imap.store(message_id, '+FLAGS', [:Seen])
  243. end
  244. end
  245. rescue Timeout::Error => e
  246. Rails.logger.error "Unable to set +FLAGS for email #{count}/#{count_all} on server (#{options[:host]}/#{options[:user]}): #{e.inspect}"
  247. raise e
  248. end
  249. count_fetched += 1
  250. end
  251. if !keep_on_server
  252. begin
  253. timeout(EXPUNGE_TIMEOUT) do
  254. @imap.expunge()
  255. end
  256. rescue Timeout::Error => e
  257. Rails.logger.error "Unable to expunge server (#{options[:host]}/#{options[:user]}): #{e.inspect}"
  258. raise e
  259. end
  260. end
  261. disconnect
  262. if count.zero?
  263. Rails.logger.info ' - no message'
  264. end
  265. if too_large_messages.present?
  266. raise too_large_messages.join("\n")
  267. end
  268. Rails.logger.info 'done'
  269. {
  270. result: result,
  271. fetched: count_fetched,
  272. notice: notice,
  273. }
  274. end
  275. def disconnect
  276. return if !@imap
  277. timeout(1.minute) do
  278. @imap.disconnect()
  279. end
  280. end
  281. =begin
  282. Channel::Driver::Imap.streamable?
  283. returns
  284. true|false
  285. =end
  286. def self.streamable?
  287. false
  288. end
  289. private
  290. def messages_is_too_old_verify?(message_meta, count, count_all)
  291. headers = parse_headers(message_meta.attr['RFC822.HEADER'])
  292. return true if !messages_is_verify_message?(headers)
  293. return true if headers['X-Zammad-Verify-Time'].blank?
  294. begin
  295. verify_time = Time.zone.parse(headers['X-Zammad-Verify-Time'])
  296. rescue => e
  297. Rails.logger.error e
  298. return true
  299. end
  300. return true if verify_time < Time.zone.now - 30.minutes
  301. Rails.logger.info " - ignore message #{count}/#{count_all} - because message has a verify message"
  302. false
  303. end
  304. def messages_is_verify_message?(headers)
  305. return true if headers['X-Zammad-Verify'] == 'true'
  306. false
  307. end
  308. def messages_is_ignore_message?(headers)
  309. return true if headers['X-Zammad-Ignore'] == 'true'
  310. false
  311. end
  312. def parse_headers(string)
  313. return {} if string.blank?
  314. headers = {}
  315. headers_pairs = string.split("\r\n")
  316. headers_pairs.each do |pair|
  317. key_value = pair.split(': ')
  318. next if key_value[0].blank?
  319. headers[key_value[0]] = key_value[1]
  320. end
  321. headers
  322. end
  323. =begin
  324. check if email is already impoted
  325. Channel::Driver::IMAP.already_imported?(message_id, message_meta, count, count_all, keep_on_server, channel)
  326. returns
  327. true|false
  328. =end
  329. # rubocop:disable Metrics/ParameterLists
  330. def already_imported?(message_id, message_meta, count, count_all, keep_on_server, channel)
  331. # rubocop:enable Metrics/ParameterLists
  332. return false if !keep_on_server
  333. return false if !message_meta.attr
  334. return false if !message_meta.attr['ENVELOPE']
  335. local_message_id = message_meta.attr['ENVELOPE'].message_id
  336. return false if local_message_id.blank?
  337. local_message_id_md5 = Digest::MD5.hexdigest(local_message_id)
  338. article = Ticket::Article.where(message_id_md5: local_message_id_md5).order('created_at DESC, id DESC').limit(1).first
  339. return false if !article
  340. # verify if message is already imported via same channel, if not, import it again
  341. ticket = article.ticket
  342. if ticket&.preferences && ticket.preferences[:channel_id].present? && channel.present?
  343. return false if ticket.preferences[:channel_id] != channel[:id]
  344. end
  345. timeout(1.minute) do
  346. @imap.store(message_id, '+FLAGS', [:Seen])
  347. end
  348. Rails.logger.info " - ignore message #{count}/#{count_all} - because message message id already imported"
  349. true
  350. end
  351. =begin
  352. check if email is already marked as deleted
  353. Channel::Driver::IMAP.deleted?(message_meta, count, count_all)
  354. returns
  355. true|false
  356. =end
  357. def deleted?(message_meta, count, count_all)
  358. return false if !message_meta.attr['FLAGS'].include?(:Deleted)
  359. Rails.logger.info " - ignore message #{count}/#{count_all} - because message has already delete flag"
  360. true
  361. end
  362. =begin
  363. check if email is to big
  364. Channel::Driver::IMAP.too_large?(message_meta, count, count_all)
  365. returns
  366. true|false
  367. =end
  368. def too_large?(message_meta)
  369. max_message_size = Setting.get('postmaster_max_size').to_f
  370. real_message_size = message_meta.attr['RFC822.SIZE'].to_f / 1024 / 1024
  371. if real_message_size > max_message_size
  372. return [real_message_size, max_message_size]
  373. end
  374. false
  375. end
  376. =begin
  377. check if channel config has changed
  378. Channel::Driver::IMAP.channel_has_changed?(channel)
  379. returns
  380. true|false
  381. =end
  382. def channel_has_changed?(channel)
  383. current_channel = Channel.find_by(id: channel.id)
  384. if !current_channel
  385. Rails.logger.info "Channel with id #{channel.id} is deleted in the meantime. Stop fetching."
  386. return true
  387. end
  388. return false if channel.updated_at == current_channel.updated_at
  389. Rails.logger.info "Channel with id #{channel.id} has changed. Stop fetching."
  390. true
  391. end
  392. =begin
  393. check if maximal fetching email count has reached
  394. Channel::Driver::IMAP.max_process_count_has_reached?(channel, count, count_max)
  395. returns
  396. true|false
  397. =end
  398. def max_process_count_has_reached?(channel, count, count_max)
  399. return false if count < count_max
  400. Rails.logger.info "Maximal fetched emails (#{count_max}) reached for this interval for Channel with id #{channel.id}."
  401. true
  402. end
  403. def timeout(seconds)
  404. Timeout.timeout(seconds) do
  405. yield
  406. end
  407. end
  408. end