email_parser.rb 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  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').tr('_', ' ').force_encoding('ISO-2022-JP').encode('UTF-8')
  415. end
  416. def message_header_hash(mail)
  417. imported_fields = mail.header.fields.map do |f|
  418. begin
  419. value = f.to_utf8
  420. if value.blank?
  421. value = f.decoded.to_utf8
  422. end
  423. # fields that cannot be cleanly parsed fallback to the empty string
  424. rescue Mail::Field::IncompleteParseError
  425. value = ''
  426. rescue
  427. value = f.decoded.to_utf8(fallback: :read_as_sanitized_binary)
  428. end
  429. [f.name.downcase, value]
  430. end.to_h
  431. # imported_fields = mail.header.fields.map { |f| [f.name.downcase, f.to_utf8] }.to_h
  432. raw_fields = mail.header.fields.index_by { |f| "raw-#{f.name.downcase}" }
  433. custom_fields = {}.tap do |h|
  434. h.replace(imported_fields.slice(*RECIPIENT_FIELDS)
  435. .transform_values { |v| v.match?(EMAIL_REGEX) ? v : '' })
  436. h['x-any-recipient'] = h.values.select(&:present?).join(', ')
  437. h['message_id'] = imported_fields['message-id']
  438. h['subject'] = imported_fields['subject']
  439. begin
  440. h['date'] = Time.zone.parse(mail.date.to_s) || imported_fields['date']
  441. rescue
  442. h['date'] = nil
  443. end
  444. end
  445. [imported_fields, raw_fields, custom_fields].reduce({}.with_indifferent_access, &:merge)
  446. end
  447. def message_body_hash(mail)
  448. message = [mail.html_part, mail.text_part, mail].find { |m| m&.body.present? }
  449. if message.present? && (message.mime_type.nil? || message.mime_type.match?(%r{^text/(plain|html)$}))
  450. content_type = message.mime_type || 'text/plain'
  451. body = body_text(message, strict_html: content_type.eql?('text/html'))
  452. end
  453. content_type = 'text/plain' if body.blank?
  454. {
  455. attachments: collect_attachments(mail),
  456. content_type: content_type || 'text/plain',
  457. body: body.presence || 'no visible content'
  458. }.with_indifferent_access
  459. end
  460. def body_text(message, **options)
  461. body_text = begin
  462. message.body.to_s
  463. rescue Mail::UnknownEncodingType # see test/data/mail/mail043.box / issue #348
  464. message.body.raw_source
  465. end
  466. body_text = body_text.utf8_encode(from: message.charset, fallback: :read_as_sanitized_binary)
  467. body_text = Mail::Utilities.to_lf(body_text)
  468. # plaintext body requires no processing
  469. return body_text if !options[:strict_html]
  470. # Issue #2390 - emails with >5k HTML links should be rejected
  471. return EXCESSIVE_LINKS_MSG if body_text.scan(/<a[[:space:]]/i).count >= 5_000
  472. body_text.html2html_strict
  473. end
  474. def collect_attachments(mail)
  475. attachments = []
  476. attachments.push(*get_nonplaintext_body_as_attachment(mail))
  477. mail.parts.each do |part|
  478. attachments.push(*gracefully_get_attachments(part, attachments, mail))
  479. end
  480. attachments
  481. end
  482. def get_nonplaintext_body_as_attachment(mail)
  483. if !(mail.html_part&.body.present? || (!mail.multipart? && mail.mime_type.present? && mail.mime_type != 'text/plain'))
  484. return
  485. end
  486. message = mail.html_part || mail
  487. if !mail.mime_type.starts_with?('text/') && mail.html_part.blank?
  488. return gracefully_get_attachments(message, [], mail)
  489. end
  490. filename = message.filename.presence || (message.mime_type.eql?('text/html') ? 'message.html' : '-no name-')
  491. headers_store = {
  492. 'content-alternative' => true,
  493. 'original-format' => message.mime_type.eql?('text/html'),
  494. 'Mime-Type' => message.mime_type,
  495. 'Charset' => message.charset,
  496. }.reject { |_, v| v.blank? }
  497. [{
  498. data: body_text(message),
  499. filename: filename,
  500. preferences: headers_store
  501. }]
  502. end
  503. def gracefully_get_attachments(part, attachments, mail)
  504. get_attachments(part, attachments, mail).flatten.compact
  505. rescue => e # Protect process to work with spam emails (see test/fixtures/mail15.box)
  506. raise e if (fail_count ||= 0).positive?
  507. (fail_count += 1) && retry
  508. end
  509. def get_attachments(file, attachments, mail)
  510. return file.parts.map { |p| get_attachments(p, attachments, mail) } if file.parts.any?
  511. return [] if [mail.text_part&.body&.encoded, mail.html_part&.body&.encoded].include?(file.body.encoded)
  512. # get file preferences
  513. headers_store = {}
  514. file.header.fields.each do |field|
  515. # full line, encode, ready for storage
  516. value = field.to_utf8
  517. if value.blank?
  518. value = field.raw_value
  519. end
  520. headers_store[field.name.to_s] = value
  521. rescue
  522. headers_store[field.name.to_s] = field.raw_value
  523. end
  524. # cleanup content id, <> will be added automatically later
  525. if headers_store['Content-ID']
  526. headers_store['Content-ID'].delete_prefix!('<')
  527. headers_store['Content-ID'].delete_suffix!('>')
  528. end
  529. # get filename from content-disposition
  530. # workaround for: NoMethodError: undefined method `filename' for #<Mail::UnstructuredField:0x007ff109e80678>
  531. begin
  532. filename = file.header[:content_disposition].try(:filename)
  533. rescue
  534. begin
  535. if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  536. filename = $3
  537. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  538. filename = $3
  539. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  540. filename = $3
  541. end
  542. rescue
  543. Rails.logger.debug { 'Unable to get filename' }
  544. end
  545. end
  546. begin
  547. if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  548. filename = $3
  549. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  550. filename = $3
  551. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  552. filename = $3
  553. end
  554. rescue
  555. Rails.logger.debug { 'Unable to get filename' }
  556. end
  557. # as fallback, use raw values
  558. if filename.blank?
  559. if headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  560. filename = $3
  561. elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  562. filename = $3
  563. elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  564. filename = $3
  565. end
  566. end
  567. # for some broken sm mail clients (X-MimeOLE: Produced By Microsoft Exchange V6.5)
  568. filename ||= file.header[:content_location].to_s.force_encoding('utf-8')
  569. # generate file name based on content-id
  570. if filename.blank? && headers_store['Content-ID'].present?
  571. if headers_store['Content-ID'] =~ /(.+?)@.+?/i
  572. filename = $1
  573. end
  574. end
  575. file_body = String.new(file.body.to_s)
  576. # generate file name based on content type
  577. if filename.blank? && headers_store['Content-Type'].present? && headers_store['Content-Type'].match?(%r{^message/rfc822}i)
  578. begin
  579. parser = Channel::EmailParser.new
  580. mail_local = parser.parse(file_body)
  581. filename = if mail_local[:subject].present?
  582. "#{mail_local[:subject]}.eml"
  583. elsif headers_store['Content-Description'].present?
  584. "#{headers_store['Content-Description']}.eml".to_s.force_encoding('utf-8')
  585. else
  586. 'Mail.eml'
  587. end
  588. rescue
  589. filename = 'Mail.eml'
  590. end
  591. end
  592. # e. g. Content-Type: video/quicktime; name="Video.MOV";
  593. if filename.blank?
  594. ['(filename|name)(\*{0,1})="(.+?)"(;|$)', '(filename|name)(\*{0,1})=\'(.+?)\'(;|$)', '(filename|name)(\*{0,1})=(.+?)(;|$)'].each do |regexp|
  595. if headers_store['Content-Type'] =~ /#{regexp}/i
  596. filename = $3
  597. break
  598. end
  599. end
  600. end
  601. # workaround for mail gem - decode filenames
  602. # https://github.com/zammad/zammad/issues/928
  603. if filename.present?
  604. filename = Mail::Encodings.value_decode(filename)
  605. end
  606. if !filename.force_encoding('UTF-8').valid_encoding?
  607. filename = filename.utf8_encode(fallback: :read_as_sanitized_binary)
  608. end
  609. # e. g. Content-Type: video/quicktime
  610. if filename.blank? && (content_type = headers_store['Content-Type'])
  611. map = {
  612. 'message/delivery-status': %w[txt delivery-status],
  613. 'text/plain': %w[txt document],
  614. 'text/html': %w[html document],
  615. 'video/quicktime': %w[mov video],
  616. 'image/jpeg': %w[jpg image],
  617. 'image/jpg': %w[jpg image],
  618. 'image/png': %w[png image],
  619. 'image/gif': %w[gif image],
  620. }
  621. map.each do |type, ext|
  622. next if !content_type.match?(/^#{Regexp.quote(type)}/i)
  623. filename = if headers_store['Content-Description'].present?
  624. "#{headers_store['Content-Description']}.#{ext[0]}".to_s.force_encoding('utf-8')
  625. else
  626. "#{ext[1]}.#{ext[0]}"
  627. end
  628. break
  629. end
  630. end
  631. # set fallback filename
  632. if filename.blank?
  633. filename = 'file'
  634. end
  635. # create uniq filename
  636. local_filename = ''
  637. local_extention = ''
  638. if filename =~ /^(.*?)\.(.+?)$/
  639. local_filename = $1
  640. local_extention = $2
  641. end
  642. 1.upto(1000) do |i|
  643. filename_exists = false
  644. attachments.each do |attachment|
  645. if attachment[:filename] == filename
  646. filename_exists = true
  647. end
  648. end
  649. break if filename_exists == false
  650. filename = if local_extention.present?
  651. "#{local_filename}#{i}.#{local_extention}"
  652. else
  653. "#{local_filename}#{i}"
  654. end
  655. end
  656. # get mime type
  657. if file.header[:content_type]&.string
  658. headers_store['Mime-Type'] = file.header[:content_type].string
  659. end
  660. # get charset
  661. if file.header&.charset
  662. headers_store['Charset'] = file.header.charset
  663. end
  664. # remove not needed header
  665. headers_store.delete('Content-Transfer-Encoding')
  666. headers_store.delete('Content-Disposition')
  667. attach = {
  668. data: file_body,
  669. filename: filename,
  670. preferences: headers_store,
  671. }
  672. [attach]
  673. end
  674. # Archive the given message as tmp/folder/md5.eml
  675. def archive_mail(folder, msg)
  676. path = Rails.root.join('tmp', folder)
  677. FileUtils.mkpath path
  678. # MD5 hash the msg and save it as "md5.eml"
  679. md5 = Digest::MD5.hexdigest(msg)
  680. file_path = Rails.root.join('tmp', folder, "#{md5}.eml")
  681. File.open(file_path, 'wb') do |file|
  682. file.write msg
  683. end
  684. file_path
  685. end
  686. # Auto reply as the postmaster to oversized emails with:
  687. # [undeliverable] Message too large
  688. def postmaster_response(channel, msg)
  689. begin
  690. reply_mail = compose_postmaster_reply(msg)
  691. rescue NotificationFactory::FileNotFoundError => e
  692. Rails.logger.error 'No valid postmaster email_oversized template found. Skipping postmaster reply. ' + e.inspect
  693. return
  694. end
  695. Rails.logger.error "Send mail too large postmaster message to: #{reply_mail[:to]}"
  696. reply_mail[:from] = EmailAddress.find_by(channel: channel).email
  697. channel.deliver(reply_mail)
  698. rescue => e
  699. Rails.logger.error "Error during sending of postmaster oversized email auto-reply: #{e.inspect}\n#{e.backtrace}"
  700. end
  701. # Compose a "Message too large" reply to the given message
  702. def compose_postmaster_reply(raw_incoming_mail, locale = nil)
  703. parsed_incoming_mail = Channel::EmailParser.new.parse(raw_incoming_mail)
  704. # construct a dummy mail object
  705. mail = OpenStruct.new
  706. mail.from_display_name = parsed_incoming_mail[:from_display_name]
  707. mail.subject = parsed_incoming_mail[:subject]
  708. mail.msg_size = format('%<MB>.2f', MB: raw_incoming_mail.size.to_f / 1024 / 1024)
  709. reply = NotificationFactory::Mailer.template(
  710. template: 'email_oversized',
  711. locale: locale,
  712. format: 'txt',
  713. objects: {
  714. mail: mail,
  715. },
  716. raw: true, # will not add application template
  717. standalone: true, # default: false - will send header & footer
  718. )
  719. reply.merge(
  720. to: parsed_incoming_mail[:from_email],
  721. body: reply[:body].gsub(/\n/, "\r\n"),
  722. content_type: 'text/plain',
  723. References: parsed_incoming_mail[:message_id],
  724. 'In-Reply-To': parsed_incoming_mail[:message_id],
  725. )
  726. end
  727. end
  728. module Mail
  729. # workaround to get content of no parseable headers - in most cases with non 7 bit ascii signs
  730. class Field
  731. def raw_value
  732. begin
  733. value = @raw_value.try(:utf8_encode)
  734. rescue
  735. value = @raw_value.utf8_encode(fallback: :read_as_sanitized_binary)
  736. end
  737. return value if value.blank?
  738. value.sub(/^.+?:(\s|)/, '')
  739. end
  740. end
  741. # issue#348 - IMAP mail fetching stops because of broken spam email (e. g. broken Content-Transfer-Encoding value see test/fixtures/mail43.box)
  742. # https://github.com/zammad/zammad/issues/348
  743. class Body
  744. def decoded
  745. if !Encodings.defined?(encoding)
  746. #raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
  747. Rails.logger.info "UnknownEncodingType: Don't know how to decode #{encoding}!"
  748. raw_source
  749. else
  750. Encodings.get_encoding(encoding).decode(raw_source)
  751. end
  752. end
  753. end
  754. end