email_parser.rb 26 KB

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