email_parser.rb 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811
  1. # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
  2. # encoding: utf-8
  3. class Channel::EmailParser
  4. PROZESS_TIME_MAX = 180
  5. EMAIL_REGEX = /.+@.+/.freeze
  6. RECIPIENT_FIELDS = %w[to cc delivered-to x-original-to envelope-to].freeze
  7. SENDER_FIELDS = %w[from reply-to return-path sender].freeze
  8. EXCESSIVE_LINKS_MSG = 'This message cannot be displayed because it contains over 5,000 links. Download the raw message below and open it via an Email client if you still wish to view it.'.freeze
  9. =begin
  10. parser = Channel::EmailParser.new
  11. mail = parser.parse(msg_as_string)
  12. mail = {
  13. from: 'Some Name <some@example.com>',
  14. from_email: 'some@example.com',
  15. from_local: 'some',
  16. from_domain: 'example.com',
  17. from_display_name: 'Some Name',
  18. message_id: 'some_message_id@example.com',
  19. to: 'Some System <system@example.com>',
  20. cc: 'Somebody <somebody@example.com>',
  21. subject: 'some message subject',
  22. body: 'some message body',
  23. content_type: 'text/html', # text/plain
  24. date: Time.zone.now,
  25. attachments: [
  26. {
  27. data: 'binary of attachment',
  28. filename: 'file_name_of_attachment.txt',
  29. preferences: {
  30. 'content-alternative' => true,
  31. 'Mime-Type' => 'text/plain',
  32. 'Charset: => 'iso-8859-1',
  33. },
  34. },
  35. ],
  36. # ignore email header
  37. x-zammad-ignore: 'false',
  38. # customer headers
  39. x-zammad-customer-login: '',
  40. x-zammad-customer-email: '',
  41. x-zammad-customer-firstname: '',
  42. x-zammad-customer-lastname: '',
  43. # ticket headers (for new tickets)
  44. x-zammad-ticket-group: 'some_group',
  45. x-zammad-ticket-state: 'some_state',
  46. x-zammad-ticket-priority: 'some_priority',
  47. x-zammad-ticket-owner: 'some_owner_login',
  48. # ticket headers (for existing tickets)
  49. x-zammad-ticket-followup-group: 'some_group',
  50. x-zammad-ticket-followup-state: 'some_state',
  51. x-zammad-ticket-followup-priority: 'some_priority',
  52. x-zammad-ticket-followup-owner: 'some_owner_login',
  53. # article headers
  54. x-zammad-article-internal: false,
  55. x-zammad-article-type: 'agent',
  56. x-zammad-article-sender: 'customer',
  57. # all other email headers
  58. some-header: 'some_value',
  59. }
  60. =end
  61. def parse(msg)
  62. mail = Mail.new(msg.force_encoding('binary'))
  63. headers = message_header_hash(mail)
  64. body = message_body_hash(mail)
  65. message_attributes = [
  66. { mail_instance: mail },
  67. headers,
  68. body,
  69. self.class.sender_attributes(headers),
  70. ]
  71. message_attributes.reduce({}.with_indifferent_access, &:merge)
  72. end
  73. =begin
  74. parser = Channel::EmailParser.new
  75. ticket, article, user, mail = parser.process(channel, email_raw_string)
  76. returns
  77. [ticket, article, user, mail]
  78. do not raise an exception - e. g. if used by scheduler
  79. parser = Channel::EmailParser.new
  80. ticket, article, user, mail = parser.process(channel, email_raw_string, false)
  81. returns
  82. [ticket, article, user, mail] || false
  83. =end
  84. def process(channel, msg, exception = true)
  85. Timeout.timeout(PROZESS_TIME_MAX) do
  86. _process(channel, msg)
  87. end
  88. rescue => e
  89. # store unprocessable email for bug reporting
  90. path = Rails.root.join('tmp', 'unprocessable_mail')
  91. FileUtils.mkpath path
  92. md5 = Digest::MD5.hexdigest(msg)
  93. filename = "#{path}/#{md5}.eml"
  94. message = "ERROR: Can't process email, you will find it for bug reporting under #{filename}, please create an issue at https://github.com/zammad/zammad/issues"
  95. p message # rubocop:disable Rails/Output
  96. p 'ERROR: ' + e.inspect # rubocop:disable Rails/Output
  97. Rails.logger.error message
  98. Rails.logger.error e
  99. File.open(filename, 'wb') do |file|
  100. file.write msg
  101. end
  102. return false if exception == false
  103. raise e.inspect + "\n" + e.backtrace.join("\n")
  104. end
  105. def _process(channel, msg)
  106. # parse email
  107. mail = parse(msg)
  108. Rails.logger.info "Process email with msgid '#{mail[:message_id]}'"
  109. # run postmaster pre filter
  110. UserInfo.current_user_id = 1
  111. filters = {}
  112. Setting.where(area: 'Postmaster::PreFilter').order(:name).each do |setting|
  113. filters[setting.name] = Setting.get(setting.name).constantize
  114. end
  115. filters.each do |key, backend|
  116. Rails.logger.debug { "run postmaster pre filter #{key}: #{backend}" }
  117. begin
  118. backend.run(channel, mail)
  119. rescue => e
  120. Rails.logger.error "can't run postmaster pre filter #{key}: #{backend}"
  121. Rails.logger.error e.inspect
  122. raise e
  123. end
  124. end
  125. # check ignore header
  126. if mail['x-zammad-ignore'.to_sym] == 'true' || mail['x-zammad-ignore'.to_sym] == true
  127. Rails.logger.info "ignored email with msgid '#{mail[:message_id]}' from '#{mail[:from]}' because of x-zammad-ignore header"
  128. return
  129. end
  130. # set interface handle
  131. original_interface_handle = ApplicationHandleInfo.current
  132. ticket = nil
  133. article = nil
  134. session_user = nil
  135. # use transaction
  136. Transaction.execute(interface_handle: "#{original_interface_handle}.postmaster") do
  137. # get sender user
  138. session_user_id = mail['x-zammad-session-user-id'.to_sym]
  139. if !session_user_id
  140. raise 'No x-zammad-session-user-id, no sender set!'
  141. end
  142. session_user = User.lookup(id: session_user_id)
  143. if !session_user
  144. raise "No user found for x-zammad-session-user-id: #{session_user_id}!"
  145. end
  146. # set current user
  147. UserInfo.current_user_id = session_user.id
  148. # get ticket# based on email headers
  149. if mail['x-zammad-ticket-id'.to_sym]
  150. ticket = Ticket.find_by(id: mail['x-zammad-ticket-id'.to_sym])
  151. end
  152. if mail['x-zammad-ticket-number'.to_sym]
  153. ticket = Ticket.find_by(number: mail['x-zammad-ticket-number'.to_sym])
  154. end
  155. # set ticket state to open if not new
  156. if ticket
  157. set_attributes_by_x_headers(ticket, 'ticket', mail, 'followup')
  158. # save changes set by x-zammad-ticket-followup-* headers
  159. ticket.save! if ticket.has_changes_to_save?
  160. state = Ticket::State.find(ticket.state_id)
  161. state_type = Ticket::StateType.find(state.state_type_id)
  162. # set ticket to open again or keep create state
  163. if !mail['x-zammad-ticket-followup-state'.to_sym] && !mail['x-zammad-ticket-followup-state_id'.to_sym]
  164. new_state = Ticket::State.find_by(default_create: true)
  165. if ticket.state_id != new_state.id && !mail['x-zammad-out-of-office'.to_sym]
  166. ticket.state = Ticket::State.find_by(default_follow_up: true)
  167. ticket.save!
  168. end
  169. end
  170. end
  171. # create new ticket
  172. if !ticket
  173. preferences = {}
  174. if channel[:id]
  175. preferences = {
  176. channel_id: channel[:id]
  177. }
  178. end
  179. # get default group where ticket is created
  180. group = nil
  181. if channel[:group_id]
  182. group = Group.lookup(id: channel[:group_id])
  183. end
  184. if group.blank? || group.active == false
  185. group = Group.where(active: true).order('id ASC').first
  186. end
  187. if group.blank?
  188. group = Group.first
  189. end
  190. title = mail[:subject]
  191. if title.blank?
  192. title = '-'
  193. end
  194. ticket = Ticket.new(
  195. group_id: group.id,
  196. title: title,
  197. preferences: preferences,
  198. )
  199. set_attributes_by_x_headers(ticket, 'ticket', mail)
  200. # create ticket
  201. ticket.save!
  202. end
  203. # apply tags to ticket
  204. if mail['x-zammad-ticket-tags'.to_sym].present?
  205. mail['x-zammad-ticket-tags'.to_sym].each do |tag|
  206. ticket.tag_add(tag)
  207. end
  208. end
  209. # set attributes
  210. ticket.with_lock do
  211. article = Ticket::Article.new(
  212. ticket_id: ticket.id,
  213. type_id: Ticket::Article::Type.find_by(name: 'email').id,
  214. sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
  215. content_type: mail[:content_type],
  216. body: mail[:body],
  217. from: mail[:from],
  218. reply_to: mail[:"reply-to"],
  219. to: mail[:to],
  220. cc: mail[:cc],
  221. subject: mail[:subject],
  222. message_id: mail[:message_id],
  223. internal: false,
  224. )
  225. # x-headers lookup
  226. set_attributes_by_x_headers(article, 'article', mail)
  227. # create article
  228. article.save!
  229. # store mail plain
  230. article.save_as_raw(msg)
  231. # store attachments
  232. mail[:attachments]&.each do |attachment|
  233. filename = attachment[:filename].force_encoding('utf-8')
  234. if !filename.force_encoding('UTF-8').valid_encoding?
  235. filename = filename.utf8_encode(fallback: :read_as_sanitized_binary)
  236. end
  237. Store.add(
  238. object: 'Ticket::Article',
  239. o_id: article.id,
  240. data: attachment[:data],
  241. filename: filename,
  242. preferences: attachment[:preferences]
  243. )
  244. end
  245. end
  246. end
  247. ticket.reload
  248. article.reload
  249. session_user.reload
  250. # run postmaster post filter
  251. filters = {}
  252. Setting.where(area: 'Postmaster::PostFilter').order(:name).each do |setting|
  253. filters[setting.name] = Setting.get(setting.name).constantize
  254. end
  255. filters.each_value do |backend|
  256. Rails.logger.debug { "run postmaster post filter #{backend}" }
  257. begin
  258. backend.run(channel, mail, ticket, article, session_user)
  259. rescue => e
  260. Rails.logger.error "can't run postmaster post filter #{backend}"
  261. Rails.logger.error e.inspect
  262. end
  263. end
  264. # return new objects
  265. [ticket, article, session_user, mail]
  266. end
  267. def self.check_attributes_by_x_headers(header_name, value)
  268. class_name = nil
  269. attribute = nil
  270. # skip check attributes if it is tags
  271. return true if header_name == 'x-zammad-ticket-tags'
  272. if header_name =~ /^x-zammad-(.+?)-(followup-|)(.*)$/i
  273. class_name = $1
  274. attribute = $3
  275. end
  276. return true if !class_name
  277. if class_name.downcase == 'article'
  278. class_name = 'Ticket::Article'
  279. end
  280. return true if !attribute
  281. key_short = attribute[ attribute.length - 3, attribute.length ]
  282. return true if key_short != '_id'
  283. class_object = class_name.to_classname.constantize
  284. return if !class_object
  285. class_instance = class_object.new
  286. return false if !class_instance.association_id_validation(attribute, value)
  287. true
  288. end
  289. def self.sender_attributes(from)
  290. if from.is_a?(HashWithIndifferentAccess)
  291. from = SENDER_FIELDS.map { |f| from[f] }.compact
  292. .map(&:to_utf8).reject(&:blank?)
  293. .partition { |address| address.match?(EMAIL_REGEX) }
  294. .flatten.first
  295. end
  296. data = {}.with_indifferent_access
  297. return data if from.blank?
  298. from = from.gsub('<>', '').strip
  299. mail_address = begin
  300. Mail::AddressList.new(from).addresses
  301. .select { |a| a.address.present? }
  302. .partition { |a| a.address.match?(EMAIL_REGEX) }
  303. .flatten.first
  304. rescue Mail::Field::ParseError => e
  305. STDOUT.puts e
  306. end
  307. if mail_address&.address.present?
  308. data[:from_email] = mail_address.address
  309. data[:from_local] = mail_address.local
  310. data[:from_domain] = mail_address.domain
  311. data[:from_display_name] = mail_address.display_name || mail_address.comments&.first
  312. elsif from =~ /^(.+?)<((.+?)@(.+?))>/
  313. data[:from_email] = $2
  314. data[:from_local] = $3
  315. data[:from_domain] = $4
  316. data[:from_display_name] = $1
  317. else
  318. data[:from_email] = from
  319. data[:from_local] = from
  320. data[:from_domain] = from
  321. data[:from_display_name] = from
  322. end
  323. # do extra decoding because we needed to use field.value
  324. data[:from_display_name] =
  325. Mail::Field.new('X-From', data[:from_display_name].to_utf8)
  326. .to_s
  327. .delete('"')
  328. .strip
  329. .gsub(/(^'|'$)/, '')
  330. data
  331. end
  332. def set_attributes_by_x_headers(item_object, header_name, mail, suffix = false)
  333. # loop all x-zammad-header-* headers
  334. item_object.attributes.each_key do |key|
  335. # ignore read only attributes
  336. next if key == 'updated_by_id'
  337. next if key == 'created_by_id'
  338. # check if id exists
  339. key_short = key[ key.length - 3, key.length ]
  340. if key_short == '_id'
  341. key_short = key[ 0, key.length - 3 ]
  342. header = "x-zammad-#{header_name}-#{key_short}"
  343. if suffix
  344. header = "x-zammad-#{header_name}-#{suffix}-#{key_short}"
  345. end
  346. # only set value on _id if value/reference lookup exists
  347. if mail[ header.to_sym ]
  348. Rails.logger.info "set_attributes_by_x_headers header #{header} found #{mail[header.to_sym]}"
  349. item_object.class.reflect_on_all_associations.map do |assoc|
  350. next if assoc.name.to_s != key_short
  351. Rails.logger.info "set_attributes_by_x_headers found #{assoc.class_name} lookup for '#{mail[header.to_sym]}'"
  352. item = assoc.class_name.constantize
  353. assoc_object = nil
  354. if item.respond_to?(:name)
  355. assoc_object = item.lookup(name: mail[header.to_sym])
  356. end
  357. if !assoc_object && item.respond_to?(:login)
  358. assoc_object = item.lookup(login: mail[header.to_sym])
  359. end
  360. if assoc_object.blank?
  361. # no assoc exists, remove header
  362. mail.delete(header.to_sym)
  363. next
  364. end
  365. Rails.logger.info "set_attributes_by_x_headers assign #{item_object.class} #{key}=#{assoc_object.id}"
  366. item_object[key] = assoc_object.id
  367. end
  368. end
  369. end
  370. # check if attribute exists
  371. header = "x-zammad-#{header_name}-#{key}"
  372. if suffix
  373. header = "x-zammad-#{header_name}-#{suffix}-#{key}"
  374. end
  375. if mail[header.to_sym]
  376. Rails.logger.info "set_attributes_by_x_headers header #{header} found. Assign #{key}=#{mail[header.to_sym]}"
  377. item_object[key] = mail[header.to_sym]
  378. end
  379. end
  380. end
  381. =begin
  382. process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
  383. Channel::EmailParser.process_unprocessable_mails
  384. =end
  385. def self.process_unprocessable_mails(params = {})
  386. path = Rails.root.join('tmp', 'unprocessable_mail')
  387. files = []
  388. Dir.glob("#{path}/*.eml") do |entry|
  389. ticket, article, user, mail = Channel::EmailParser.new.process(params, IO.binread(entry))
  390. next if ticket.blank?
  391. files.push entry
  392. File.delete(entry)
  393. end
  394. files
  395. end
  396. private
  397. def message_header_hash(mail)
  398. imported_fields = mail.header.fields.map do |f|
  399. begin
  400. value = f.to_utf8
  401. if value.blank?
  402. value = f.raw_value.to_utf8
  403. end
  404. rescue
  405. value = f.raw_value.to_utf8(fallback: :read_as_sanitized_binary)
  406. end
  407. [f.name.downcase, value]
  408. end.to_h
  409. # imported_fields = mail.header.fields.map { |f| [f.name.downcase, f.to_utf8] }.to_h
  410. raw_fields = mail.header.fields.map { |f| ["raw-#{f.name.downcase}", f] }.to_h
  411. custom_fields = {}.tap do |h|
  412. h.replace(imported_fields.slice(*RECIPIENT_FIELDS)
  413. .transform_values { |v| v.match?(EMAIL_REGEX) ? v : '' })
  414. h['x-any-recipient'] = h.values.select(&:present?).join(', ')
  415. h['message_id'] = imported_fields['message-id']
  416. h['subject'] = imported_fields['subject']
  417. begin
  418. h['date'] = Time.zone.parse(mail.date.to_s) || imported_fields['date']
  419. rescue
  420. h['date'] = nil
  421. end
  422. end
  423. [imported_fields, raw_fields, custom_fields].reduce({}.with_indifferent_access, &:merge)
  424. end
  425. def message_body_hash(mail)
  426. message = [mail.html_part, mail.text_part, mail].find { |m| m&.body.present? }
  427. if message.present? && (message.mime_type.nil? || message.mime_type.match?(%r{^text/(plain|html)$}))
  428. content_type = message.mime_type || 'text/plain'
  429. body = body_text(message, strict_html: content_type.eql?('text/html'))
  430. end
  431. content_type = 'text/plain' if body.blank?
  432. {
  433. attachments: collect_attachments(mail),
  434. content_type: content_type || 'text/plain',
  435. body: body.presence || 'no visible content'
  436. }.with_indifferent_access
  437. end
  438. def body_text(message, **options)
  439. body_text = begin
  440. message.body.to_s
  441. rescue Mail::UnknownEncodingType # see test/data/mail/mail043.box / issue #348
  442. message.body.raw_source
  443. end
  444. body_text = body_text.utf8_encode(from: message.charset, fallback: :read_as_sanitized_binary)
  445. body_text = Mail::Utilities.to_lf(body_text)
  446. # plaintext body requires no processing
  447. return body_text if !options[:strict_html]
  448. # Issue #2390 - emails with >5k HTML links should be rejected
  449. return EXCESSIVE_LINKS_MSG if body_text.scan(/<a[[:space:]]/i).count >= 5_000
  450. body_text.html2html_strict
  451. end
  452. def collect_attachments(mail)
  453. attachments = []
  454. # Add non-plaintext body as an attachment
  455. if mail.html_part&.body.present? ||
  456. (!mail.multipart? && mail.mime_type.present? && mail.mime_type != 'text/plain')
  457. message = mail.html_part || mail
  458. filename = message.filename.presence ||
  459. (message.mime_type.eql?('text/html') ? 'message.html' : '-no name-')
  460. headers_store = {
  461. 'content-alternative' => true,
  462. 'original-format' => message.mime_type.eql?('text/html'),
  463. 'Mime-Type' => message.mime_type,
  464. 'Charset' => message.charset,
  465. }.reject { |_, v| v.blank? }
  466. attachments.push({ data: body_text(message),
  467. filename: filename,
  468. preferences: headers_store })
  469. end
  470. mail.parts.each do |part|
  471. begin
  472. new_attachments = get_attachments(part, attachments, mail).flatten.compact
  473. attachments.push(*new_attachments)
  474. rescue => e # Protect process to work with spam emails (see test/fixtures/mail15.box)
  475. raise e if (fail_count ||= 0).positive?
  476. (fail_count += 1) && retry
  477. end
  478. end
  479. attachments
  480. end
  481. def get_attachments(file, attachments, mail)
  482. return file.parts.map { |p| get_attachments(p, attachments, mail) } if file.parts.any?
  483. return [] if [mail.text_part&.body&.encoded, mail.html_part&.body&.encoded].include?(file.body.encoded)
  484. # get file preferences
  485. headers_store = {}
  486. file.header.fields.each do |field|
  487. # full line, encode, ready for storage
  488. begin
  489. value = field.to_utf8
  490. if value.blank?
  491. value = field.raw_value
  492. end
  493. headers_store[field.name.to_s] = value
  494. rescue => e
  495. headers_store[field.name.to_s] = field.raw_value
  496. end
  497. end
  498. # cleanup content id, <> will be added automatically later
  499. if headers_store['Content-ID']
  500. headers_store['Content-ID'].gsub!(/^</, '')
  501. headers_store['Content-ID'].gsub!(/>$/, '')
  502. end
  503. # get filename from content-disposition
  504. # workaround for: NoMethodError: undefined method `filename' for #<Mail::UnstructuredField:0x007ff109e80678>
  505. begin
  506. filename = file.header[:content_disposition].try(:filename)
  507. rescue
  508. begin
  509. if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  510. filename = $3
  511. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  512. filename = $3
  513. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  514. filename = $3
  515. end
  516. rescue
  517. Rails.logger.debug { 'Unable to get filename' }
  518. end
  519. end
  520. begin
  521. if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  522. filename = $3
  523. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  524. filename = $3
  525. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  526. filename = $3
  527. end
  528. rescue
  529. Rails.logger.debug { 'Unable to get filename' }
  530. end
  531. # as fallback, use raw values
  532. if filename.blank?
  533. if headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  534. filename = $3
  535. elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  536. filename = $3
  537. elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  538. filename = $3
  539. end
  540. end
  541. # for some broken sm mail clients (X-MimeOLE: Produced By Microsoft Exchange V6.5)
  542. filename ||= file.header[:content_location].to_s
  543. # generate file name based on content-id
  544. if filename.blank? && headers_store['Content-ID'].present?
  545. if headers_store['Content-ID'] =~ /(.+?)@.+?/i
  546. filename = $1
  547. end
  548. end
  549. # generate file name based on content type
  550. if filename.blank? && headers_store['Content-Type'].present?
  551. if headers_store['Content-Type'].match?(%r{^message/rfc822}i)
  552. begin
  553. parser = Channel::EmailParser.new
  554. mail_local = parser.parse(file.body.to_s)
  555. filename = if mail_local[:subject].present?
  556. "#{mail_local[:subject]}.eml"
  557. elsif headers_store['Content-Description'].present?
  558. "#{headers_store['Content-Description']}.eml".to_s.force_encoding('utf-8')
  559. else
  560. 'Mail.eml'
  561. end
  562. rescue
  563. filename = 'Mail.eml'
  564. end
  565. end
  566. # e. g. Content-Type: video/quicktime; name="Video.MOV";
  567. if filename.blank?
  568. ['(filename|name)(\*{0,1})="(.+?)"(;|$)', '(filename|name)(\*{0,1})=\'(.+?)\'(;|$)', '(filename|name)(\*{0,1})=(.+?)(;|$)'].each do |regexp|
  569. if headers_store['Content-Type'] =~ /#{regexp}/i
  570. filename = $3
  571. break
  572. end
  573. end
  574. end
  575. # e. g. Content-Type: video/quicktime
  576. if filename.blank?
  577. map = {
  578. 'message/delivery-status': ['txt', 'delivery-status'],
  579. 'text/plain': %w[txt document],
  580. 'text/html': %w[html document],
  581. 'video/quicktime': %w[mov video],
  582. 'image/jpeg': %w[jpg image],
  583. 'image/jpg': %w[jpg image],
  584. 'image/png': %w[png image],
  585. 'image/gif': %w[gif image],
  586. }
  587. map.each do |type, ext|
  588. next if headers_store['Content-Type'] !~ /^#{Regexp.quote(type)}/i
  589. filename = if headers_store['Content-Description'].present?
  590. "#{headers_store['Content-Description']}.#{ext[0]}".to_s.force_encoding('utf-8')
  591. else
  592. "#{ext[1]}.#{ext[0]}"
  593. end
  594. break
  595. end
  596. end
  597. end
  598. if filename.blank?
  599. filename = 'file'
  600. end
  601. local_filename = ''
  602. local_extention = ''
  603. if filename =~ /^(.*?)\.(.+?)$/
  604. local_filename = $1
  605. local_extention = $2
  606. end
  607. 1.upto(1000) do |i|
  608. filename_exists = false
  609. attachments.each do |attachment|
  610. if attachment[:filename] == filename
  611. filename_exists = true
  612. end
  613. end
  614. break if filename_exists == false
  615. filename = if local_extention.present?
  616. "#{local_filename}#{i}.#{local_extention}"
  617. else
  618. "#{local_filename}#{i}"
  619. end
  620. end
  621. # get mime type
  622. if file.header[:content_type]&.string
  623. headers_store['Mime-Type'] = file.header[:content_type].string
  624. end
  625. # get charset
  626. if file.header&.charset
  627. headers_store['Charset'] = file.header.charset
  628. end
  629. # remove not needed header
  630. headers_store.delete('Content-Transfer-Encoding')
  631. headers_store.delete('Content-Disposition')
  632. # workaround for mail gem
  633. # https://github.com/zammad/zammad/issues/928
  634. filename = Mail::Encodings.value_decode(filename)
  635. attach = {
  636. data: file.body.to_s,
  637. filename: filename,
  638. preferences: headers_store,
  639. }
  640. [attach]
  641. end
  642. end
  643. module Mail
  644. # workaround to get content of no parseable headers - in most cases with non 7 bit ascii signs
  645. class Field
  646. def raw_value
  647. begin
  648. value = @raw_value.try(:utf8_encode)
  649. rescue
  650. value = @raw_value.utf8_encode(fallback: :read_as_sanitized_binary)
  651. end
  652. return value if value.blank?
  653. value.sub(/^.+?:(\s|)/, '')
  654. end
  655. end
  656. # issue#348 - IMAP mail fetching stops because of broken spam email (e. g. broken Content-Transfer-Encoding value see test/fixtures/mail43.box)
  657. # https://github.com/zammad/zammad/issues/348
  658. class Body
  659. def decoded
  660. if !Encodings.defined?(encoding)
  661. #raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
  662. Rails.logger.info "UnknownEncodingType: Don't know how to decode #{encoding}!"
  663. raw_source
  664. else
  665. Encodings.get_encoding(encoding).decode(raw_source)
  666. end
  667. end
  668. end
  669. end