email_parser.rb 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932
  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. msg = msg.force_encoding('binary')
  63. # mail 2.6 and earlier accepted non-conforming mails that lacked the correct CRLF seperators,
  64. # mail 2.7 and above require CRLF so we force it on using binary_unsafe_to_crlf
  65. msg = Mail::Utilities.binary_unsafe_to_crlf(msg)
  66. mail = Mail.new(msg)
  67. force_parts_encoding_if_needed(mail)
  68. headers = message_header_hash(mail)
  69. body = message_body_hash(mail)
  70. message_attributes = [
  71. { mail_instance: mail },
  72. headers,
  73. body,
  74. self.class.sender_attributes(headers),
  75. { raw: msg },
  76. ]
  77. message_attributes.reduce({}.with_indifferent_access, &:merge)
  78. end
  79. =begin
  80. parser = Channel::EmailParser.new
  81. ticket, article, user, mail = parser.process(channel, email_raw_string)
  82. returns
  83. [ticket, article, user, mail]
  84. do not raise an exception - e. g. if used by scheduler
  85. parser = Channel::EmailParser.new
  86. ticket, article, user, mail = parser.process(channel, email_raw_string, false)
  87. returns
  88. [ticket, article, user, mail] || false
  89. =end
  90. def process(channel, msg, exception = true)
  91. Timeout.timeout(PROZESS_TIME_MAX) do
  92. _process(channel, msg)
  93. end
  94. rescue => e
  95. # store unprocessable email for bug reporting
  96. filename = archive_mail('unprocessable_mail', msg)
  97. message = "Can't process email, you will find it for bug reporting under #{filename}, please create an issue at https://github.com/zammad/zammad/issues"
  98. p "ERROR: #{message}" # rubocop:disable Rails/Output
  99. p "ERROR: #{e.inspect}" # rubocop:disable Rails/Output
  100. Rails.logger.error message
  101. Rails.logger.error e
  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'] == 'true' || mail[:'x-zammad-ignore'] == 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']
  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']
  150. ticket = Ticket.find_by(id: mail[:'x-zammad-ticket-id'])
  151. end
  152. if mail[:'x-zammad-ticket-number']
  153. ticket = Ticket.find_by(number: mail[:'x-zammad-ticket-number'])
  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. # set ticket to open again or keep create state
  161. if !mail[:'x-zammad-ticket-followup-state'] && !mail[:'x-zammad-ticket-followup-state_id']
  162. new_state = Ticket::State.find_by(default_create: true)
  163. if ticket.state_id != new_state.id && !mail[:'x-zammad-out-of-office']
  164. ticket.state = Ticket::State.find_by(default_follow_up: true)
  165. ticket.save!
  166. end
  167. end
  168. end
  169. # create new ticket
  170. if !ticket
  171. preferences = {}
  172. if channel[:id]
  173. preferences = {
  174. channel_id: channel[:id]
  175. }
  176. end
  177. # get default group where ticket is created
  178. group = nil
  179. if channel[:group_id]
  180. group = Group.lookup(id: channel[:group_id])
  181. end
  182. if group.blank? || group.active == false
  183. group = Group.where(active: true).order(id: :asc).first
  184. end
  185. if group.blank?
  186. group = Group.first
  187. end
  188. title = mail[:subject]
  189. if title.blank?
  190. title = '-'
  191. end
  192. ticket = Ticket.new(
  193. group_id: group.id,
  194. title: title,
  195. preferences: preferences,
  196. )
  197. set_attributes_by_x_headers(ticket, 'ticket', mail)
  198. # create ticket
  199. ticket.save!
  200. end
  201. # apply tags to ticket
  202. if mail[:'x-zammad-ticket-tags'].present?
  203. mail[:'x-zammad-ticket-tags'].each do |tag|
  204. ticket.tag_add(tag)
  205. end
  206. end
  207. # set attributes
  208. ticket.with_lock do
  209. article = Ticket::Article.new(
  210. ticket_id: ticket.id,
  211. type_id: Ticket::Article::Type.find_by(name: 'email').id,
  212. sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
  213. content_type: mail[:content_type],
  214. body: mail[:body],
  215. from: mail[:from],
  216. reply_to: mail[:"reply-to"],
  217. to: mail[:to],
  218. cc: mail[:cc],
  219. subject: mail[:subject],
  220. message_id: mail[:message_id],
  221. internal: false,
  222. )
  223. # x-headers lookup
  224. set_attributes_by_x_headers(article, 'article', mail)
  225. # create article
  226. article.save!
  227. # store mail plain
  228. article.save_as_raw(msg)
  229. # store attachments
  230. mail[:attachments]&.each do |attachment|
  231. filename = attachment[:filename].force_encoding('utf-8')
  232. if !filename.force_encoding('UTF-8').valid_encoding?
  233. filename = filename.utf8_encode(fallback: :read_as_sanitized_binary)
  234. end
  235. Store.add(
  236. object: 'Ticket::Article',
  237. o_id: article.id,
  238. data: attachment[:data],
  239. filename: filename,
  240. preferences: attachment[:preferences]
  241. )
  242. end
  243. end
  244. end
  245. ticket.reload
  246. article.reload
  247. session_user.reload
  248. # run postmaster post filter
  249. filters = {}
  250. Setting.where(area: 'Postmaster::PostFilter').order(:name).each do |setting|
  251. filters[setting.name] = Setting.get(setting.name).constantize
  252. end
  253. filters.each_value do |backend|
  254. Rails.logger.debug { "run postmaster post filter #{backend}" }
  255. begin
  256. backend.run(channel, mail, ticket, article, session_user)
  257. rescue => e
  258. Rails.logger.error "can't run postmaster post filter #{backend}"
  259. Rails.logger.error e.inspect
  260. end
  261. end
  262. # return new objects
  263. [ticket, article, session_user, mail]
  264. end
  265. def self.check_attributes_by_x_headers(header_name, value)
  266. class_name = nil
  267. attribute = nil
  268. # skip check attributes if it is tags
  269. return true if header_name == 'x-zammad-ticket-tags'
  270. if header_name =~ /^x-zammad-(.+?)-(followup-|)(.*)$/i
  271. class_name = $1
  272. attribute = $3
  273. end
  274. return true if !class_name
  275. if class_name.casecmp('article').zero?
  276. class_name = 'Ticket::Article'
  277. end
  278. return true if !attribute
  279. key_short = attribute[ attribute.length - 3, attribute.length ]
  280. return true if key_short != '_id'
  281. class_object = class_name.to_classname.constantize
  282. return if !class_object
  283. class_instance = class_object.new
  284. return false if !class_instance.association_id_validation(attribute, value)
  285. true
  286. end
  287. def self.sender_attributes(from)
  288. if from.is_a?(HashWithIndifferentAccess)
  289. from = SENDER_FIELDS.map { |f| from[f] }.compact
  290. .map(&:to_utf8).reject(&:blank?)
  291. .partition { |address| address.match?(EMAIL_REGEX) }
  292. .flatten.first
  293. end
  294. data = {}.with_indifferent_access
  295. return data if from.blank?
  296. from = from.gsub('<>', '').strip
  297. mail_address = begin
  298. Mail::AddressList.new(from).addresses
  299. .select { |a| a.address.present? }
  300. .partition { |a| a.address.match?(EMAIL_REGEX) }
  301. .flatten.first
  302. rescue Mail::Field::ParseError => e
  303. $stdout.puts e
  304. end
  305. if mail_address&.address.present?
  306. data[:from_email] = mail_address.address
  307. data[:from_local] = mail_address.local
  308. data[:from_domain] = mail_address.domain
  309. data[:from_display_name] = mail_address.display_name || mail_address.comments&.first
  310. elsif from =~ /^(.+?)<((.+?)@(.+?))>/
  311. data[:from_email] = $2
  312. data[:from_local] = $3
  313. data[:from_domain] = $4
  314. data[:from_display_name] = $1
  315. else
  316. data[:from_email] = from
  317. data[:from_local] = from
  318. data[:from_domain] = from
  319. data[:from_display_name] = from
  320. end
  321. # do extra decoding because we needed to use field.value
  322. data[:from_display_name] =
  323. Mail::Field.new('X-From', data[:from_display_name].to_utf8)
  324. .to_s
  325. .delete('"')
  326. .strip
  327. .gsub(/(^'|'$)/, '')
  328. data
  329. end
  330. def set_attributes_by_x_headers(item_object, header_name, mail, suffix = false)
  331. # loop all x-zammad-header-* headers
  332. item_object.attributes.each_key do |key|
  333. # ignore read only attributes
  334. next if key == 'updated_by_id'
  335. next if key == 'created_by_id'
  336. # check if id exists
  337. key_short = key[ key.length - 3, key.length ]
  338. if key_short == '_id'
  339. key_short = key[ 0, key.length - 3 ]
  340. header = "x-zammad-#{header_name}-#{key_short}"
  341. if suffix
  342. header = "x-zammad-#{header_name}-#{suffix}-#{key_short}"
  343. end
  344. # only set value on _id if value/reference lookup exists
  345. if mail[header.to_sym]
  346. Rails.logger.info "set_attributes_by_x_headers header #{header} found #{mail[header.to_sym]}"
  347. item_object.class.reflect_on_all_associations.map do |assoc|
  348. next if assoc.name.to_s != key_short
  349. Rails.logger.info "set_attributes_by_x_headers found #{assoc.class_name} lookup for '#{mail[header.to_sym]}'"
  350. item = assoc.class_name.constantize
  351. assoc_object = nil
  352. if item.new.respond_to?(:name)
  353. assoc_object = item.lookup(name: mail[header.to_sym])
  354. end
  355. if !assoc_object && item.new.respond_to?(:login)
  356. assoc_object = item.lookup(login: mail[header.to_sym])
  357. end
  358. if !assoc_object && item.new.respond_to?(:email)
  359. assoc_object = item.lookup(email: mail[header.to_sym])
  360. end
  361. if assoc_object.blank?
  362. # no assoc exists, remove header
  363. mail.delete(header.to_sym)
  364. next
  365. end
  366. Rails.logger.info "set_attributes_by_x_headers assign #{item_object.class} #{key}=#{assoc_object.id}"
  367. item_object[key] = assoc_object.id
  368. end
  369. end
  370. end
  371. # check if attribute exists
  372. header = "x-zammad-#{header_name}-#{key}"
  373. if suffix
  374. header = "x-zammad-#{header_name}-#{suffix}-#{key}"
  375. end
  376. if mail[header.to_sym]
  377. Rails.logger.info "set_attributes_by_x_headers header #{header} found. Assign #{key}=#{mail[header.to_sym]}"
  378. item_object[key] = mail[header.to_sym]
  379. end
  380. end
  381. end
  382. =begin
  383. process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
  384. Channel::EmailParser.process_unprocessable_mails
  385. =end
  386. def self.process_unprocessable_mails(params = {})
  387. path = Rails.root.join('tmp/unprocessable_mail')
  388. files = []
  389. Dir.glob("#{path}/*.eml") do |entry|
  390. ticket, _article, _user, _mail = Channel::EmailParser.new.process(params, IO.binread(entry))
  391. next if ticket.blank?
  392. files.push entry
  393. File.delete(entry)
  394. end
  395. files
  396. end
  397. =begin
  398. process oversized emails by:
  399. 1. Archiving the oversized mail as tmp/oversized_mail/md5.eml
  400. 2. Reply with a postmaster message to inform the sender
  401. =end
  402. def process_oversized_mail(channel, msg)
  403. archive_mail('oversized_mail', msg)
  404. postmaster_response(channel, msg)
  405. end
  406. private
  407. # https://github.com/zammad/zammad/issues/2922
  408. def force_parts_encoding_if_needed(mail)
  409. mail.parts.each { |elem| force_single_part_encoding_if_needed(elem) }
  410. end
  411. # https://github.com/zammad/zammad/issues/2922
  412. def force_single_part_encoding_if_needed(part)
  413. return if part.charset != 'iso-2022-jp'
  414. part.body = part.body.encoded.unpack1('M').force_encoding('ISO-2022-JP').encode('UTF-8')
  415. end
  416. ISO2022JP_REGEXP = /=\?ISO-2022-JP\?B\?(.+?)\?=/.freeze
  417. # https://github.com/zammad/zammad/issues/3115
  418. def header_field_unpack_japanese(field)
  419. field.value.gsub ISO2022JP_REGEXP do
  420. Base64.decode64($1).force_encoding('SJIS').encode('UTF-8')
  421. end
  422. end
  423. def message_header_hash(mail)
  424. imported_fields = mail.header.fields.map do |f|
  425. begin
  426. value = if f.value.match?(ISO2022JP_REGEXP)
  427. header_field_unpack_japanese(f)
  428. else
  429. f.to_utf8
  430. end
  431. if value.blank?
  432. value = f.decoded.to_utf8
  433. end
  434. # fields that cannot be cleanly parsed fallback to the empty string
  435. rescue Mail::Field::IncompleteParseError
  436. value = ''
  437. rescue
  438. value = f.decoded.to_utf8(fallback: :read_as_sanitized_binary)
  439. end
  440. [f.name.downcase, value]
  441. end.to_h
  442. # imported_fields = mail.header.fields.map { |f| [f.name.downcase, f.to_utf8] }.to_h
  443. raw_fields = mail.header.fields.index_by { |f| "raw-#{f.name.downcase}" }
  444. custom_fields = {}.tap do |h|
  445. h.replace(imported_fields.slice(*RECIPIENT_FIELDS)
  446. .transform_values { |v| v.match?(EMAIL_REGEX) ? v : '' })
  447. h['x-any-recipient'] = h.values.select(&:present?).join(', ')
  448. h['message_id'] = imported_fields['message-id']
  449. h['subject'] = imported_fields['subject']
  450. begin
  451. h['date'] = Time.zone.parse(mail.date.to_s) || imported_fields['date']
  452. rescue
  453. h['date'] = nil
  454. end
  455. end
  456. [imported_fields, raw_fields, custom_fields].reduce({}.with_indifferent_access, &:merge)
  457. end
  458. def message_body_hash(mail)
  459. message = [mail.html_part, mail.text_part, mail].find { |m| m&.body.present? }
  460. if message.present? && (message.mime_type.nil? || message.mime_type.match?(%r{^text/(plain|html)$}))
  461. content_type = message.mime_type || 'text/plain'
  462. body = body_text(message, strict_html: content_type.eql?('text/html'))
  463. end
  464. content_type = 'text/plain' if body.blank?
  465. {
  466. attachments: collect_attachments(mail),
  467. content_type: content_type || 'text/plain',
  468. body: body.presence || 'no visible content'
  469. }.with_indifferent_access
  470. end
  471. def body_text(message, **options)
  472. body_text = begin
  473. message.body.to_s
  474. rescue Mail::UnknownEncodingType # see test/data/mail/mail043.box / issue #348
  475. message.body.raw_source
  476. end
  477. body_text = body_text.utf8_encode(from: message.charset, fallback: :read_as_sanitized_binary)
  478. body_text = Mail::Utilities.to_lf(body_text)
  479. # plaintext body requires no processing
  480. return body_text if !options[:strict_html]
  481. # Issue #2390 - emails with >5k HTML links should be rejected
  482. return EXCESSIVE_LINKS_MSG if body_text.scan(/<a[[:space:]]/i).count >= 5_000
  483. body_text.html2html_strict
  484. end
  485. def collect_attachments(mail)
  486. attachments = []
  487. attachments.push(*get_nonplaintext_body_as_attachment(mail))
  488. mail.parts.each do |part|
  489. attachments.push(*gracefully_get_attachments(part, attachments, mail))
  490. end
  491. attachments
  492. end
  493. def get_nonplaintext_body_as_attachment(mail)
  494. if !(mail.html_part&.body.present? || (!mail.multipart? && mail.mime_type.present? && mail.mime_type != 'text/plain'))
  495. return
  496. end
  497. message = mail.html_part || mail
  498. if !mail.mime_type.starts_with?('text/') && mail.html_part.blank?
  499. return gracefully_get_attachments(message, [], mail)
  500. end
  501. filename = message.filename.presence || (message.mime_type.eql?('text/html') ? 'message.html' : '-no name-')
  502. headers_store = {
  503. 'content-alternative' => true,
  504. 'original-format' => message.mime_type.eql?('text/html'),
  505. 'Mime-Type' => message.mime_type,
  506. 'Charset' => message.charset,
  507. }.reject { |_, v| v.blank? }
  508. [{
  509. data: body_text(message),
  510. filename: filename,
  511. preferences: headers_store
  512. }]
  513. end
  514. def gracefully_get_attachments(part, attachments, mail)
  515. get_attachments(part, attachments, mail).flatten.compact
  516. rescue => e # Protect process to work with spam emails (see test/fixtures/mail15.box)
  517. raise e if (fail_count ||= 0).positive?
  518. (fail_count += 1) && retry
  519. end
  520. def get_attachments(file, attachments, mail)
  521. return file.parts.map { |p| get_attachments(p, attachments, mail) } if file.parts.any?
  522. return [] if [mail.text_part&.body&.encoded, mail.html_part&.body&.encoded].include?(file.body.encoded)
  523. # get file preferences
  524. headers_store = {}
  525. file.header.fields.each do |field|
  526. # full line, encode, ready for storage
  527. value = field.to_utf8
  528. if value.blank?
  529. value = field.raw_value
  530. end
  531. headers_store[field.name.to_s] = value
  532. rescue
  533. headers_store[field.name.to_s] = field.raw_value
  534. end
  535. # cleanup content id, <> will be added automatically later
  536. if headers_store['Content-ID']
  537. headers_store['Content-ID'].delete_prefix!('<')
  538. headers_store['Content-ID'].delete_suffix!('>')
  539. end
  540. # get filename from content-disposition
  541. # workaround for: NoMethodError: undefined method `filename' for #<Mail::UnstructuredField:0x007ff109e80678>
  542. begin
  543. filename = file.header[:content_disposition].try(:filename)
  544. rescue
  545. begin
  546. if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  547. filename = $3
  548. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  549. filename = $3
  550. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  551. filename = $3
  552. end
  553. rescue
  554. Rails.logger.debug { 'Unable to get filename' }
  555. end
  556. end
  557. begin
  558. if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  559. filename = $3
  560. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  561. filename = $3
  562. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  563. filename = $3
  564. end
  565. rescue
  566. Rails.logger.debug { 'Unable to get filename' }
  567. end
  568. # as fallback, use raw values
  569. if filename.blank?
  570. if headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  571. filename = $3
  572. elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  573. filename = $3
  574. elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  575. filename = $3
  576. end
  577. end
  578. # for some broken sm mail clients (X-MimeOLE: Produced By Microsoft Exchange V6.5)
  579. filename ||= file.header[:content_location].to_s.force_encoding('utf-8')
  580. # generate file name based on content-id
  581. if filename.blank? && headers_store['Content-ID'].present? && headers_store['Content-ID'] =~ /(.+?)@.+?/i
  582. filename = $1
  583. end
  584. file_body = String.new(file.body.to_s)
  585. # generate file name based on content type
  586. if filename.blank? && headers_store['Content-Type'].present? && headers_store['Content-Type'].match?(%r{^message/rfc822}i)
  587. begin
  588. parser = Channel::EmailParser.new
  589. mail_local = parser.parse(file_body)
  590. filename = if mail_local[:subject].present?
  591. "#{mail_local[:subject]}.eml"
  592. elsif headers_store['Content-Description'].present?
  593. "#{headers_store['Content-Description']}.eml".to_s.force_encoding('utf-8')
  594. else
  595. 'Mail.eml'
  596. end
  597. rescue
  598. filename = 'Mail.eml'
  599. end
  600. end
  601. # e. g. Content-Type: video/quicktime; name="Video.MOV";
  602. if filename.blank?
  603. ['(filename|name)(\*{0,1})="(.+?)"(;|$)', '(filename|name)(\*{0,1})=\'(.+?)\'(;|$)', '(filename|name)(\*{0,1})=(.+?)(;|$)'].each do |regexp|
  604. if headers_store['Content-Type'] =~ /#{regexp}/i
  605. filename = $3
  606. break
  607. end
  608. end
  609. end
  610. # workaround for mail gem - decode filenames
  611. # https://github.com/zammad/zammad/issues/928
  612. if filename.present?
  613. filename = Mail::Encodings.value_decode(filename)
  614. end
  615. if !filename.force_encoding('UTF-8').valid_encoding?
  616. filename = filename.utf8_encode(fallback: :read_as_sanitized_binary)
  617. end
  618. # e. g. Content-Type: video/quicktime
  619. if filename.blank? && (content_type = headers_store['Content-Type'])
  620. map = {
  621. 'message/delivery-status': %w[txt delivery-status],
  622. 'text/plain': %w[txt document],
  623. 'text/html': %w[html document],
  624. 'video/quicktime': %w[mov video],
  625. 'image/jpeg': %w[jpg image],
  626. 'image/jpg': %w[jpg image],
  627. 'image/png': %w[png image],
  628. 'image/gif': %w[gif image],
  629. }
  630. map.each do |type, ext|
  631. next if !content_type.match?(/^#{Regexp.quote(type)}/i)
  632. filename = if headers_store['Content-Description'].present?
  633. "#{headers_store['Content-Description']}.#{ext[0]}".to_s.force_encoding('utf-8')
  634. else
  635. "#{ext[1]}.#{ext[0]}"
  636. end
  637. break
  638. end
  639. end
  640. # set fallback filename
  641. if filename.blank?
  642. filename = 'file'
  643. end
  644. # create uniq filename
  645. local_filename = ''
  646. local_extention = ''
  647. if filename =~ /^(.*?)\.(.+?)$/
  648. local_filename = $1
  649. local_extention = $2
  650. end
  651. 1.upto(1000) do |i|
  652. filename_exists = false
  653. attachments.each do |attachment|
  654. if attachment[:filename] == filename
  655. filename_exists = true
  656. end
  657. end
  658. break if filename_exists == false
  659. filename = if local_extention.present?
  660. "#{local_filename}#{i}.#{local_extention}"
  661. else
  662. "#{local_filename}#{i}"
  663. end
  664. end
  665. # get mime type
  666. if file.header[:content_type]&.string
  667. headers_store['Mime-Type'] = file.header[:content_type].string
  668. end
  669. # get charset
  670. if file.header&.charset
  671. headers_store['Charset'] = file.header.charset
  672. end
  673. # remove not needed header
  674. headers_store.delete('Content-Transfer-Encoding')
  675. headers_store.delete('Content-Disposition')
  676. attach = {
  677. data: file_body,
  678. filename: filename,
  679. preferences: headers_store,
  680. }
  681. [attach]
  682. end
  683. # Archive the given message as tmp/folder/md5.eml
  684. def archive_mail(folder, msg)
  685. path = Rails.root.join('tmp', folder)
  686. FileUtils.mkpath path
  687. # MD5 hash the msg and save it as "md5.eml"
  688. md5 = Digest::MD5.hexdigest(msg)
  689. file_path = Rails.root.join('tmp', folder, "#{md5}.eml")
  690. File.open(file_path, 'wb') do |file|
  691. file.write msg
  692. end
  693. file_path
  694. end
  695. # Auto reply as the postmaster to oversized emails with:
  696. # [undeliverable] Message too large
  697. def postmaster_response(channel, msg)
  698. begin
  699. reply_mail = compose_postmaster_reply(msg)
  700. rescue NotificationFactory::FileNotFoundError => e
  701. Rails.logger.error "No valid postmaster email_oversized template found. Skipping postmaster reply. #{e.inspect}"
  702. return
  703. end
  704. Rails.logger.error "Send mail too large postmaster message to: #{reply_mail[:to]}"
  705. reply_mail[:from] = EmailAddress.find_by(channel: channel).email
  706. channel.deliver(reply_mail)
  707. rescue => e
  708. Rails.logger.error "Error during sending of postmaster oversized email auto-reply: #{e.inspect}\n#{e.backtrace}"
  709. end
  710. # Compose a "Message too large" reply to the given message
  711. def compose_postmaster_reply(raw_incoming_mail, locale = nil)
  712. parsed_incoming_mail = Channel::EmailParser.new.parse(raw_incoming_mail)
  713. # construct a dummy mail object
  714. mail = OpenStruct.new
  715. mail.from_display_name = parsed_incoming_mail[:from_display_name]
  716. mail.subject = parsed_incoming_mail[:subject]
  717. mail.msg_size = format('%<MB>.2f', MB: raw_incoming_mail.size.to_f / 1024 / 1024)
  718. reply = NotificationFactory::Mailer.template(
  719. template: 'email_oversized',
  720. locale: locale,
  721. format: 'txt',
  722. objects: {
  723. mail: mail,
  724. },
  725. raw: true, # will not add application template
  726. standalone: true, # default: false - will send header & footer
  727. )
  728. reply.merge(
  729. to: parsed_incoming_mail[:from_email],
  730. body: reply[:body].gsub(/\n/, "\r\n"),
  731. content_type: 'text/plain',
  732. References: parsed_incoming_mail[:message_id],
  733. 'In-Reply-To': parsed_incoming_mail[:message_id],
  734. )
  735. end
  736. end
  737. module Mail
  738. # workaround to get content of no parseable headers - in most cases with non 7 bit ascii signs
  739. class Field
  740. def raw_value
  741. begin
  742. value = @raw_value.try(:utf8_encode)
  743. rescue
  744. value = @raw_value.utf8_encode(fallback: :read_as_sanitized_binary)
  745. end
  746. return value if value.blank?
  747. value.sub(/^.+?:(\s|)/, '')
  748. end
  749. end
  750. # issue#348 - IMAP mail fetching stops because of broken spam email (e. g. broken Content-Transfer-Encoding value see test/fixtures/mail43.box)
  751. # https://github.com/zammad/zammad/issues/348
  752. class Body
  753. def decoded
  754. if !Encodings.defined?(encoding)
  755. #raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
  756. Rails.logger.info "UnknownEncodingType: Don't know how to decode #{encoding}!"
  757. raw_source
  758. else
  759. Encodings.get_encoding(encoding).decode(raw_source)
  760. end
  761. end
  762. end
  763. end