imap.rb 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
  2. require 'net/imap'
  3. class Channel::Driver::Imap < Channel::EmailParser
  4. def fetchable?(_channel)
  5. true
  6. end
  7. =begin
  8. fetch emails from IMAP account
  9. instance = Channel::Driver::Imap.new
  10. result = instance.fetch(params[:inbound][:options], channel, 'verify', subject_looking_for)
  11. returns
  12. {
  13. result: 'ok',
  14. fetched: 123,
  15. notice: 'e. g. message about to big emails in mailbox',
  16. }
  17. check if connect to IMAP account is possible, return count of mails in mailbox
  18. instance = Channel::Driver::Imap.new
  19. result = instance.fetch(params[:inbound][:options], channel, 'check')
  20. returns
  21. {
  22. result: 'ok',
  23. content_messages: 123,
  24. }
  25. verify IMAP account, check if search email is in there
  26. instance = Channel::Driver::Imap.new
  27. result = instance.fetch(params[:inbound][:options], channel, 'verify', subject_looking_for)
  28. returns
  29. {
  30. result: 'ok', # 'verify not ok'
  31. }
  32. example
  33. params = {
  34. host: 'outlook.office365.com',
  35. user: 'xxx@znuny.onmicrosoft.com',
  36. password: 'xxx',
  37. keep_on_server: true,
  38. }
  39. channel = Channel.last
  40. instance = Channel::Driver::Imap.new
  41. result = instance.fetch(params, channel, 'verify')
  42. =end
  43. def fetch(options, channel, check_type = '', verify_string = '')
  44. ssl = true
  45. starttls = false
  46. port = 993
  47. keep_on_server = false
  48. folder = 'INBOX'
  49. if options[:keep_on_server] == true || options[:keep_on_server] == 'true'
  50. keep_on_server = true
  51. end
  52. if options.key?(:ssl) && options[:ssl] == false
  53. ssl = false
  54. port = 143
  55. end
  56. port = if options.key?(:port) && options[:port].present?
  57. options[:port].to_i
  58. elsif ssl == true
  59. 993
  60. else
  61. 143
  62. end
  63. if ssl == true && port != 993
  64. ssl = false
  65. starttls = true
  66. end
  67. if options[:folder].present?
  68. folder = options[:folder]
  69. end
  70. Rails.logger.info "fetching imap (#{options[:host]}/#{options[:user]} port=#{port},ssl=#{ssl},starttls=#{starttls},folder=#{folder},keep_on_server=#{keep_on_server})"
  71. # on check, reduce open_timeout to have faster probing
  72. timeout = 45
  73. if check_type == 'check'
  74. timeout = 6
  75. end
  76. Timeout.timeout(timeout) do
  77. @imap = ::Net::IMAP.new(options[:host], port, ssl, nil, false)
  78. if starttls
  79. @imap.starttls()
  80. end
  81. end
  82. @imap.login(options[:user], options[:password])
  83. # select folder
  84. @imap.select(folder)
  85. # sort messages by date on server (if not supported), if not fetch messages via search (first in, first out)
  86. filter = ['ALL']
  87. if keep_on_server && check_type != 'check' && check_type != 'verify'
  88. filter = %w[NOT SEEN]
  89. end
  90. begin
  91. message_ids = @imap.sort(['DATE'], filter, 'US-ASCII')
  92. rescue
  93. message_ids = @imap.search(filter)
  94. end
  95. # check mode only
  96. if check_type == 'check'
  97. Rails.logger.info 'check only mode, fetch no emails'
  98. content_max_check = 2
  99. content_messages = 0
  100. # check messages
  101. message_ids.each do |message_id|
  102. message_meta = @imap.fetch(message_id, ['RFC822.HEADER'])[0].attr
  103. # check how many content messages we have, for notice used
  104. header = message_meta['RFC822.HEADER']
  105. if header && header !~ /x-zammad-ignore/i
  106. content_messages += 1
  107. break if content_max_check < content_messages
  108. end
  109. end
  110. if content_messages >= content_max_check
  111. content_messages = message_ids.count
  112. end
  113. disconnect
  114. return {
  115. result: 'ok',
  116. content_messages: content_messages,
  117. }
  118. end
  119. # reverse message order to increase performance
  120. if check_type == 'verify'
  121. Rails.logger.info "verify mode, fetch no emails #{verify_string}"
  122. message_ids.reverse!
  123. # check for verify message
  124. message_ids.each do |message_id|
  125. message_meta = @imap.fetch(message_id, ['ENVELOPE'])[0].attr
  126. # check if verify message exists
  127. subject = message_meta['ENVELOPE'].subject
  128. next if !subject
  129. next if subject !~ /#{verify_string}/
  130. Rails.logger.info " - verify email #{verify_string} found"
  131. @imap.store(message_id, '+FLAGS', [:Deleted])
  132. @imap.expunge()
  133. disconnect
  134. return {
  135. result: 'ok',
  136. }
  137. end
  138. disconnect
  139. return {
  140. result: 'verify not ok',
  141. }
  142. end
  143. # fetch regular messages
  144. count_all = message_ids.count
  145. count = 0
  146. count_fetched = 0
  147. notice = ''
  148. message_ids.each do |message_id|
  149. count += 1
  150. Rails.logger.info " - message #{count}/#{count_all}"
  151. message_meta = @imap.fetch(message_id, ['RFC822.SIZE', 'ENVELOPE', 'FLAGS', 'INTERNALDATE'])[0]
  152. # ignore to big messages
  153. info = too_big?(message_meta, count, count_all)
  154. if info
  155. notice += "#{info}\n"
  156. next
  157. end
  158. # ignore deleted messages
  159. next if deleted?(message_meta, count, count_all)
  160. # ignore already imported
  161. next if already_imported?(message_id, message_meta, count, count_all, keep_on_server, channel)
  162. # delete email from server after article was created
  163. msg = @imap.fetch(message_id, 'RFC822')[0].attr['RFC822']
  164. next if !msg
  165. process(channel, msg, false)
  166. if !keep_on_server
  167. @imap.store(message_id, '+FLAGS', [:Deleted])
  168. else
  169. @imap.store(message_id, '+FLAGS', [:Seen])
  170. end
  171. count_fetched += 1
  172. end
  173. if !keep_on_server
  174. @imap.expunge()
  175. end
  176. disconnect
  177. if count.zero?
  178. Rails.logger.info ' - no message'
  179. end
  180. Rails.logger.info 'done'
  181. {
  182. result: 'ok',
  183. fetched: count_fetched,
  184. notice: notice,
  185. }
  186. end
  187. def disconnect
  188. return if !@imap
  189. @imap.disconnect()
  190. end
  191. =begin
  192. Channel::Driver::Imap.streamable?
  193. returns
  194. true|false
  195. =end
  196. def self.streamable?
  197. false
  198. end
  199. private
  200. # rubocop:disable Metrics/ParameterLists
  201. def already_imported?(message_id, message_meta, count, count_all, keep_on_server, channel)
  202. # rubocop:enable Metrics/ParameterLists
  203. return false if !keep_on_server
  204. return false if !message_meta.attr
  205. return false if !message_meta.attr['ENVELOPE']
  206. local_message_id = message_meta.attr['ENVELOPE'].message_id
  207. return false if local_message_id.blank?
  208. local_message_id_md5 = Digest::MD5.hexdigest(local_message_id)
  209. article = Ticket::Article.where(message_id_md5: local_message_id_md5).order('created_at DESC, id DESC').limit(1).first
  210. return false if !article
  211. # verify if message is already imported via same channel, if not, import it again
  212. ticket = article.ticket
  213. if ticket&.preferences && ticket.preferences[:channel_id].present? && channel.present?
  214. return false if ticket.preferences[:channel_id] != channel[:id]
  215. end
  216. @imap.store(message_id, '+FLAGS', [:Seen])
  217. Rails.logger.info " - ignore message #{count}/#{count_all} - because message message id already imported"
  218. true
  219. end
  220. def deleted?(message_meta, count, count_all)
  221. return false if !message_meta.attr['FLAGS'].include?(:Deleted)
  222. Rails.logger.info " - ignore message #{count}/#{count_all} - because message has already delete flag"
  223. true
  224. end
  225. def too_big?(message_meta, count, count_all)
  226. max_message_size = Setting.get('postmaster_max_size').to_f
  227. real_message_size = message_meta.attr['RFC822.SIZE'].to_f / 1024 / 1024
  228. if real_message_size > max_message_size
  229. info = " - ignore message #{count}/#{count_all} - because message is too big (is:#{real_message_size} MB/max:#{max_message_size} MB)"
  230. Rails.logger.info info
  231. return info
  232. end
  233. false
  234. end
  235. end