imap.rb 13 KB

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