imap.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. require 'net/imap'
  3. class Channel::Driver::Imap < Channel::Driver::BaseEmailInbound
  4. FETCH_METADATA_TIMEOUT = 2.minutes
  5. FETCH_MSG_TIMEOUT = 4.minutes
  6. LIST_MESSAGES_TIMEOUT = 6.minutes
  7. EXPUNGE_TIMEOUT = 16.minutes
  8. DEFAULT_TIMEOUT = 45.seconds
  9. CHECK_ONLY_TIMEOUT = 8.seconds
  10. FETCH_COUNT_MAX = 5_000
  11. # Fetches emails from IMAP server
  12. #
  13. # @param options [Hash]
  14. # @option options [String] :folder to fetch emails from
  15. # @option options [String] :user to login with
  16. # @option options [String] :password to login with
  17. # @option options [String] :host
  18. # @option options [Integer, String] :port
  19. # @option options [Boolean] :ssl_verify
  20. # @option options [String] :ssl off to turn off ssl
  21. # @option options [String] :auth_type XOAUTH2 for Google/Microsoft365 or fitting authentication type for other
  22. # @param channel [Channel]
  23. #
  24. # @return [Hash]
  25. #
  26. # {
  27. # result: 'ok',
  28. # fetched: 123,
  29. # notice: 'e. g. message about to big emails in mailbox',
  30. # }
  31. #
  32. # @example
  33. #
  34. # params = {
  35. # user: 'xxx@example.com',
  36. # password: 'xxx',
  37. # host: 'example'com'
  38. # }
  39. #
  40. # channel = Channel.last
  41. # instance = Channel::Driver::Pop3.new
  42. # result = instance.fetch(params, channel)
  43. def fetch(...) # rubocop:disable Lint/UselessMethodDefinition
  44. # fetch() method is defined in superclass, but options are subclass-specific,
  45. # so define it here for documentation purposes.
  46. super
  47. end
  48. def fetch_all_message_ids
  49. fetch_message_ids %w[ALL]
  50. end
  51. def fetch_unread_message_ids
  52. fetch_message_ids %w[NOT SEEN]
  53. rescue
  54. fetch_message_ids %w[UNSEEN]
  55. end
  56. def fetch_message_ids(filter)
  57. raise if @imap.capabilities.exclude?('SORT')
  58. {
  59. result: @imap.sort(['DATE'], filter, 'US-ASCII'),
  60. is_fallback: false
  61. }
  62. rescue
  63. {
  64. result: @imap.search(filter),
  65. is_fallback: true # indicates that we can not use a result ordered by date
  66. }
  67. end
  68. def fetch_message_body_key(options)
  69. # https://github.com/zammad/zammad/issues/4589
  70. options['host'] == 'imap.mail.me.com' ? 'BODY[]' : 'RFC822'
  71. end
  72. def disconnect
  73. return if !@imap
  74. Timeout.timeout(1.minute) do
  75. @imap.disconnect
  76. end
  77. end
  78. # Parses RFC822 header
  79. # @param [String] RFC822 header text blob
  80. # @return [Hash<String=>String>]
  81. def self.parse_rfc822_headers(string)
  82. array = string
  83. .gsub("\r\n\t", ' ') # Some servers (e.g. microsoft365) may put attribute value on a separate line and tab it
  84. .lines(chomp: true)
  85. .map { |line| line.split(%r{:\s*}, 2).map(&:strip) }
  86. array.each { |elem| elem.append(nil) if elem.one? }
  87. Hash[*array.flatten]
  88. end
  89. # Parses RFC822 header
  90. # @param [Net::IMAP::FetchData] fetched message
  91. # @return [Hash<String=>String>]
  92. def self.extract_rfc822_headers(message_meta)
  93. blob = message_meta&.attr&.dig 'RFC822.HEADER'
  94. return if !blob
  95. parse_rfc822_headers blob
  96. end
  97. private
  98. =begin
  99. check if email is already marked as deleted
  100. Channel::Driver::IMAP.deleted?(message_meta, count, count_all)
  101. returns
  102. true|false
  103. =end
  104. def deleted?(message_meta, count, count_all)
  105. return false if message_meta.attr['FLAGS'].exclude?(:Deleted)
  106. Rails.logger.info " - ignore message #{count}/#{count_all} - because message has already delete flag"
  107. true
  108. end
  109. def setup_connection(options, check: false)
  110. server_settings = setup_connection_server_settings(options)
  111. setup_connection_server_log(server_settings)
  112. Certificate::ApplySSLCertificates.ensure_fresh_ssl_context if server_settings[:ssl_or_starttls]
  113. # on check, reduce open_timeout to have faster probing
  114. timeout = check ? CHECK_ONLY_TIMEOUT : DEFAULT_TIMEOUT
  115. @imap = Timeout.timeout(timeout) do
  116. Net::IMAP.new(server_settings[:host], port: server_settings[:port], ssl: server_settings[:ssl_settings])
  117. .tap do |conn|
  118. next if server_settings[:ssl_or_starttls] != :starttls
  119. conn.starttls(verify_mode: server_settings[:ssl_verify] ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE)
  120. end
  121. end
  122. Timeout.timeout(timeout) do
  123. if server_settings[:auth_type].present?
  124. @imap.authenticate(server_settings[:auth_type], server_settings[:user], server_settings[:password])
  125. else
  126. @imap.login(server_settings[:user], server_settings[:password].dup&.force_encoding('ascii-8bit'))
  127. end
  128. end
  129. Timeout.timeout(timeout) do
  130. # select folder
  131. @imap.select(server_settings[:folder])
  132. end
  133. @imap
  134. end
  135. def setup_connection_server_log(server_settings)
  136. settings = [
  137. "#{server_settings[:host]}/#{server_settings[:user]} port=#{server_settings[:port]}",
  138. "ssl=#{server_settings[:ssl_or_starttls] == :ssl}",
  139. "starttls=#{server_settings[:ssl_or_starttls] == :starttls}",
  140. "folder=#{server_settings[:folder]}",
  141. "keep_on_server=#{server_settings[:keep_on_server]}",
  142. "auth_type=#{server_settings.fetch(:auth_type, 'LOGIN')}",
  143. "ssl_verify=#{server_settings[:ssl_verify]}"
  144. ]
  145. Rails.logger.info "fetching imap (#{settings.join(',')}"
  146. end
  147. def setup_connection_server_settings(options)
  148. ssl_or_starttls = setup_connection_ssl_or_starttls(options)
  149. ssl_verify = options.fetch(:ssl_verify, true)
  150. ssl_settings = setup_connection_ssl_settings(ssl_or_starttls, ssl_verify)
  151. options
  152. .slice(:host, :user, :password, :auth_type)
  153. .merge(
  154. ssl_or_starttls:,
  155. ssl_verify:,
  156. ssl_settings:,
  157. port: setup_connection_port(options, ssl_or_starttls),
  158. folder: options[:folder].presence || 'INBOX',
  159. keep_on_server: ActiveModel::Type::Boolean.new.cast(options[:keep_on_server]),
  160. )
  161. end
  162. def setup_connection_ssl_settings(ssl_or_starttls, ssl_verify)
  163. if ssl_or_starttls != :ssl
  164. false
  165. elsif ssl_verify
  166. true
  167. else
  168. { verify_mode: OpenSSL::SSL::VERIFY_NONE }
  169. end
  170. end
  171. def setup_connection_ssl_or_starttls(options)
  172. case options[:ssl]
  173. when 'off'
  174. false
  175. when 'starttls'
  176. :starttls
  177. else
  178. :ssl
  179. end
  180. end
  181. def setup_connection_port(options, ssl_or_starttls)
  182. if options.key?(:port) && options[:port].present?
  183. options[:port].to_i
  184. elsif ssl_or_starttls == :ssl
  185. 993
  186. else
  187. 143
  188. end
  189. end
  190. def fetch_single_message(message_id, count, count_all) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  191. message_meta = Timeout.timeout(FETCH_METADATA_TIMEOUT) do
  192. @imap.fetch(message_id, ['RFC822.SIZE', 'FLAGS', 'INTERNALDATE', 'RFC822.HEADER'])[0]
  193. rescue Net::IMAP::ResponseParseError => e
  194. raise if e.message.exclude?('unknown token')
  195. notice += <<~NOTICE
  196. One of your incoming emails could not be imported (#{e.message}).
  197. Please remove it from your inbox directly
  198. to prevent Zammad from trying to import it again.
  199. NOTICE
  200. Rails.logger.error "Net::IMAP failed to parse message #{message_id}: #{e.message} (#{e.class})"
  201. Rails.logger.error '(See https://github.com/zammad/zammad/issues/2754 for more details)'
  202. return MessageResult.new(success: false, after_action: [:result_error, notice])
  203. end
  204. return MessageResult.new(sucess: false) if message_meta.nil?
  205. message_validator = MessageValidator.new(self.class.extract_rfc822_headers(message_meta), message_meta.attr['RFC822.SIZE'])
  206. if message_validator.fresh_verify_message?
  207. Rails.logger.info " - ignore message #{count}/#{count_all} - because message has a verify message"
  208. return MessageResult.new(sucess: false)
  209. end
  210. # ignore deleted messages
  211. if deleted?(message_meta, count, count_all)
  212. return MessageResult.new(sucess: false)
  213. end
  214. # ignore already imported
  215. if message_validator.already_imported?(@keep_on_server, @channel)
  216. Timeout.timeout(1.minute) do
  217. @imap.store(message_id, '+FLAGS', [:Seen])
  218. end
  219. Rails.logger.info " - ignore message #{count}/#{count_all} - because message message id already imported"
  220. return MessageResult.new(sucess: false)
  221. end
  222. # delete email from server after article was created
  223. msg = begin
  224. Timeout.timeout(FETCH_MSG_TIMEOUT) do
  225. key = fetch_message_body_key(@options)
  226. @imap.fetch(message_id, key)[0].attr[key]
  227. end
  228. rescue Timeout::Error => e
  229. Rails.logger.error "Unable to fetch email from #{count}/#{count_all} from server (#{@options[:host]}/#{@options[:user]}): #{e.inspect}"
  230. raise e
  231. end
  232. if !msg
  233. return MessageResult.new(sucess: false)
  234. end
  235. # do not process too big messages, instead download & send postmaster reply
  236. if (too_large_info = message_validator.too_large?)
  237. if Setting.get('postmaster_send_reject_if_mail_too_large') == true
  238. 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)"
  239. Rails.logger.info info
  240. after_action = [:notice, "#{info}\n"]
  241. process_oversized_mail(@channel, msg)
  242. else
  243. info = " - ignore message #{count}/#{count_all} - because message is too large (is:#{too_large_info[0]} MB/max:#{too_large_info[1]} MB)"
  244. Rails.logger.info info
  245. return MessageResult.new(success: false, after_action: [:too_large_ignored, "#{info}\n"])
  246. end
  247. else
  248. process(@channel, msg, false)
  249. end
  250. begin
  251. Timeout.timeout(FETCH_MSG_TIMEOUT) do
  252. if @keep_on_server
  253. @imap.store(message_id, '+FLAGS', [:Seen])
  254. else
  255. @imap.store(message_id, '+FLAGS', [:Deleted])
  256. end
  257. end
  258. rescue Timeout::Error => e
  259. Rails.logger.error "Unable to set +FLAGS for email #{count}/#{count_all} on server (#{@options[:host]}/#{@options[:user]}): #{e.inspect}"
  260. raise e
  261. end
  262. MessageResult.new(success: true, after_action: after_action)
  263. end
  264. def messages_iterator(keep_on_server, _options, reverse: false)
  265. message_ids_result = Timeout.timeout(LIST_MESSAGES_TIMEOUT) do
  266. if keep_on_server
  267. fetch_unread_message_ids
  268. else
  269. fetch_all_message_ids
  270. end
  271. end
  272. ids = message_ids_result[:result]
  273. ids.reverse! if reverse
  274. [ids.first(FETCH_COUNT_MAX), ids.count]
  275. end
  276. def fetch_wrap_up
  277. if !@keep_on_server
  278. begin
  279. Timeout.timeout(EXPUNGE_TIMEOUT) do
  280. @imap.expunge
  281. end
  282. rescue Timeout::Error => e
  283. Rails.logger.error "Unable to expunge server (#{@options[:host]}/#{@options[:user]}): #{e.inspect}"
  284. raise e
  285. end
  286. end
  287. disconnect
  288. end
  289. def check_single_message(message_id)
  290. message_meta = Timeout.timeout(FETCH_METADATA_TIMEOUT) do
  291. @imap.fetch(message_id, ['RFC822.HEADER'])[0]
  292. end
  293. MessageValidator.new(self.class.extract_rfc822_headers(message_meta))
  294. end
  295. def verify_single_message(message_id, verify_string)
  296. message_meta = Timeout.timeout(FETCH_METADATA_TIMEOUT) do
  297. @imap.fetch(message_id, ['RFC822.HEADER'])[0]
  298. end
  299. # check if verify message exists
  300. headers = self.class.extract_rfc822_headers(message_meta)
  301. headers['Subject']&.match?(%r{#{verify_string}})
  302. end
  303. def verify_message_cleanup(message_id)
  304. Timeout.timeout(EXPUNGE_TIMEOUT) do
  305. @imap.store(message_id, '+FLAGS', [:Deleted])
  306. @imap.expunge
  307. end
  308. end
  309. end