email_parser.rb 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  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. headers = message_header_hash(mail)
  68. body = message_body_hash(mail)
  69. message_attributes = [
  70. { mail_instance: mail },
  71. headers,
  72. body,
  73. self.class.sender_attributes(headers),
  74. ]
  75. message_attributes.reduce({}.with_indifferent_access, &:merge)
  76. end
  77. =begin
  78. parser = Channel::EmailParser.new
  79. ticket, article, user, mail = parser.process(channel, email_raw_string)
  80. returns
  81. [ticket, article, user, mail]
  82. do not raise an exception - e. g. if used by scheduler
  83. parser = Channel::EmailParser.new
  84. ticket, article, user, mail = parser.process(channel, email_raw_string, false)
  85. returns
  86. [ticket, article, user, mail] || false
  87. =end
  88. def process(channel, msg, exception = true)
  89. Timeout.timeout(PROZESS_TIME_MAX) do
  90. _process(channel, msg)
  91. end
  92. rescue => e
  93. # store unprocessable email for bug reporting
  94. filename = archive_mail('unprocessable_mail', msg)
  95. 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"
  96. p message # rubocop:disable Rails/Output
  97. p 'ERROR: ' + e.inspect # rubocop:disable Rails/Output
  98. Rails.logger.error message
  99. Rails.logger.error e
  100. return false if exception == false
  101. raise e.inspect + "\n" + e.backtrace.join("\n")
  102. end
  103. def _process(channel, msg)
  104. # parse email
  105. mail = parse(msg)
  106. Rails.logger.info "Process email with msgid '#{mail[:message_id]}'"
  107. # run postmaster pre filter
  108. UserInfo.current_user_id = 1
  109. filters = {}
  110. Setting.where(area: 'Postmaster::PreFilter').order(:name).each do |setting|
  111. filters[setting.name] = Setting.get(setting.name).constantize
  112. end
  113. filters.each do |key, backend|
  114. Rails.logger.debug { "run postmaster pre filter #{key}: #{backend}" }
  115. begin
  116. backend.run(channel, mail)
  117. rescue => e
  118. Rails.logger.error "can't run postmaster pre filter #{key}: #{backend}"
  119. Rails.logger.error e.inspect
  120. raise e
  121. end
  122. end
  123. # check ignore header
  124. if mail['x-zammad-ignore'.to_sym] == 'true' || mail['x-zammad-ignore'.to_sym] == true
  125. Rails.logger.info "ignored email with msgid '#{mail[:message_id]}' from '#{mail[:from]}' because of x-zammad-ignore header"
  126. return
  127. end
  128. # set interface handle
  129. original_interface_handle = ApplicationHandleInfo.current
  130. ticket = nil
  131. article = nil
  132. session_user = nil
  133. # use transaction
  134. Transaction.execute(interface_handle: "#{original_interface_handle}.postmaster") do
  135. # get sender user
  136. session_user_id = mail['x-zammad-session-user-id'.to_sym]
  137. if !session_user_id
  138. raise 'No x-zammad-session-user-id, no sender set!'
  139. end
  140. session_user = User.lookup(id: session_user_id)
  141. if !session_user
  142. raise "No user found for x-zammad-session-user-id: #{session_user_id}!"
  143. end
  144. # set current user
  145. UserInfo.current_user_id = session_user.id
  146. # get ticket# based on email headers
  147. if mail['x-zammad-ticket-id'.to_sym]
  148. ticket = Ticket.find_by(id: mail['x-zammad-ticket-id'.to_sym])
  149. end
  150. if mail['x-zammad-ticket-number'.to_sym]
  151. ticket = Ticket.find_by(number: mail['x-zammad-ticket-number'.to_sym])
  152. end
  153. # set ticket state to open if not new
  154. if ticket
  155. set_attributes_by_x_headers(ticket, 'ticket', mail, 'followup')
  156. # save changes set by x-zammad-ticket-followup-* headers
  157. ticket.save! if ticket.has_changes_to_save?
  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] = Setting.get(setting.name).constantize
  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.casecmp('article').zero?
  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 = class_name.to_classname.constantize
  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.new.respond_to?(:name)
  351. assoc_object = item.lookup(name: mail[header.to_sym])
  352. end
  353. if !assoc_object && item.new.respond_to?(:login)
  354. assoc_object = item.lookup(login: mail[header.to_sym])
  355. end
  356. if !assoc_object && item.new.respond_to?(:email)
  357. assoc_object = item.lookup(email: mail[header.to_sym])
  358. end
  359. if assoc_object.blank?
  360. # no assoc exists, remove header
  361. mail.delete(header.to_sym)
  362. next
  363. end
  364. Rails.logger.info "set_attributes_by_x_headers assign #{item_object.class} #{key}=#{assoc_object.id}"
  365. item_object[key] = assoc_object.id
  366. end
  367. end
  368. end
  369. # check if attribute exists
  370. header = "x-zammad-#{header_name}-#{key}"
  371. if suffix
  372. header = "x-zammad-#{header_name}-#{suffix}-#{key}"
  373. end
  374. if mail[header.to_sym]
  375. Rails.logger.info "set_attributes_by_x_headers header #{header} found. Assign #{key}=#{mail[header.to_sym]}"
  376. item_object[key] = mail[header.to_sym]
  377. end
  378. end
  379. end
  380. =begin
  381. process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
  382. Channel::EmailParser.process_unprocessable_mails
  383. =end
  384. def self.process_unprocessable_mails(params = {})
  385. path = Rails.root.join('tmp/unprocessable_mail')
  386. files = []
  387. Dir.glob("#{path}/*.eml") do |entry|
  388. ticket, _article, _user, _mail = Channel::EmailParser.new.process(params, IO.binread(entry))
  389. next if ticket.blank?
  390. files.push entry
  391. File.delete(entry)
  392. end
  393. files
  394. end
  395. =begin
  396. process oversized emails by:
  397. 1. Archiving the oversized mail as tmp/oversized_mail/md5.eml
  398. 2. Reply with a postmaster message to inform the sender
  399. =end
  400. def process_oversized_mail(channel, msg)
  401. archive_mail('oversized_mail', msg)
  402. postmaster_response(channel, msg)
  403. end
  404. private
  405. def message_header_hash(mail)
  406. imported_fields = mail.header.fields.map do |f|
  407. begin
  408. value = f.to_utf8
  409. if value.blank?
  410. value = f.decoded.to_utf8
  411. end
  412. # fields that cannot be cleanly parsed fallback to the empty string
  413. rescue Mail::Field::IncompleteParseError
  414. value = ''
  415. rescue
  416. value = f.decoded.to_utf8(fallback: :read_as_sanitized_binary)
  417. end
  418. [f.name.downcase, value]
  419. end.to_h
  420. # imported_fields = mail.header.fields.map { |f| [f.name.downcase, f.to_utf8] }.to_h
  421. raw_fields = mail.header.fields.map { |f| ["raw-#{f.name.downcase}", f] }.to_h
  422. custom_fields = {}.tap do |h|
  423. h.replace(imported_fields.slice(*RECIPIENT_FIELDS)
  424. .transform_values { |v| v.match?(EMAIL_REGEX) ? v : '' })
  425. h['x-any-recipient'] = h.values.select(&:present?).join(', ')
  426. h['message_id'] = imported_fields['message-id']
  427. h['subject'] = imported_fields['subject']
  428. begin
  429. h['date'] = Time.zone.parse(mail.date.to_s) || imported_fields['date']
  430. rescue
  431. h['date'] = nil
  432. end
  433. end
  434. [imported_fields, raw_fields, custom_fields].reduce({}.with_indifferent_access, &:merge)
  435. end
  436. def message_body_hash(mail)
  437. message = [mail.html_part, mail.text_part, mail].find { |m| m&.body.present? }
  438. if message.present? && (message.mime_type.nil? || message.mime_type.match?(%r{^text/(plain|html)$}))
  439. content_type = message.mime_type || 'text/plain'
  440. body = body_text(message, strict_html: content_type.eql?('text/html'))
  441. end
  442. content_type = 'text/plain' if body.blank?
  443. {
  444. attachments: collect_attachments(mail),
  445. content_type: content_type || 'text/plain',
  446. body: body.presence || 'no visible content'
  447. }.with_indifferent_access
  448. end
  449. def body_text(message, **options)
  450. body_text = begin
  451. message.body.to_s
  452. rescue Mail::UnknownEncodingType # see test/data/mail/mail043.box / issue #348
  453. message.body.raw_source
  454. end
  455. body_text = body_text.utf8_encode(from: message.charset, fallback: :read_as_sanitized_binary)
  456. body_text = Mail::Utilities.to_lf(body_text)
  457. # plaintext body requires no processing
  458. return body_text if !options[:strict_html]
  459. # Issue #2390 - emails with >5k HTML links should be rejected
  460. return EXCESSIVE_LINKS_MSG if body_text.scan(/<a[[:space:]]/i).count >= 5_000
  461. body_text.html2html_strict
  462. end
  463. def collect_attachments(mail)
  464. attachments = []
  465. attachments.push(*get_nonplaintext_body_as_attachment(mail))
  466. mail.parts.each do |part|
  467. attachments.push(*gracefully_get_attachments(part, attachments, mail))
  468. end
  469. attachments
  470. end
  471. def get_nonplaintext_body_as_attachment(mail)
  472. if !(mail.html_part&.body.present? || (!mail.multipart? && mail.mime_type.present? && mail.mime_type != 'text/plain'))
  473. return
  474. end
  475. message = mail.html_part || mail
  476. if !mail.mime_type.starts_with?('text/') && mail.html_part.blank?
  477. return gracefully_get_attachments(message, [], mail)
  478. end
  479. filename = message.filename.presence || (message.mime_type.eql?('text/html') ? 'message.html' : '-no name-')
  480. headers_store = {
  481. 'content-alternative' => true,
  482. 'original-format' => message.mime_type.eql?('text/html'),
  483. 'Mime-Type' => message.mime_type,
  484. 'Charset' => message.charset,
  485. }.reject { |_, v| v.blank? }
  486. [{
  487. data: body_text(message),
  488. filename: filename,
  489. preferences: headers_store
  490. }]
  491. end
  492. def gracefully_get_attachments(part, attachments, mail)
  493. get_attachments(part, attachments, mail).flatten.compact
  494. rescue => e # Protect process to work with spam emails (see test/fixtures/mail15.box)
  495. raise e if (fail_count ||= 0).positive?
  496. (fail_count += 1) && retry
  497. end
  498. def get_attachments(file, attachments, mail)
  499. return file.parts.map { |p| get_attachments(p, attachments, mail) } if file.parts.any?
  500. return [] if [mail.text_part&.body&.encoded, mail.html_part&.body&.encoded].include?(file.body.encoded)
  501. # get file preferences
  502. headers_store = {}
  503. file.header.fields.each do |field|
  504. # full line, encode, ready for storage
  505. value = field.to_utf8
  506. if value.blank?
  507. value = field.raw_value
  508. end
  509. headers_store[field.name.to_s] = value
  510. rescue
  511. headers_store[field.name.to_s] = field.raw_value
  512. end
  513. # cleanup content id, <> will be added automatically later
  514. if headers_store['Content-ID']
  515. headers_store['Content-ID'].gsub!(/^</, '')
  516. headers_store['Content-ID'].gsub!(/>$/, '')
  517. end
  518. # get filename from content-disposition
  519. # workaround for: NoMethodError: undefined method `filename' for #<Mail::UnstructuredField:0x007ff109e80678>
  520. begin
  521. filename = file.header[:content_disposition].try(:filename)
  522. rescue
  523. begin
  524. if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  525. filename = $3
  526. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  527. filename = $3
  528. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  529. filename = $3
  530. end
  531. rescue
  532. Rails.logger.debug { 'Unable to get filename' }
  533. end
  534. end
  535. begin
  536. if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  537. filename = $3
  538. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  539. filename = $3
  540. elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  541. filename = $3
  542. end
  543. rescue
  544. Rails.logger.debug { 'Unable to get filename' }
  545. end
  546. # as fallback, use raw values
  547. if filename.blank?
  548. if headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
  549. filename = $3
  550. elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
  551. filename = $3
  552. elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
  553. filename = $3
  554. end
  555. end
  556. # for some broken sm mail clients (X-MimeOLE: Produced By Microsoft Exchange V6.5)
  557. filename ||= file.header[:content_location].to_s.force_encoding('utf-8')
  558. # generate file name based on content-id
  559. if filename.blank? && headers_store['Content-ID'].present?
  560. if headers_store['Content-ID'] =~ /(.+?)@.+?/i
  561. filename = $1
  562. end
  563. end
  564. # generate file name based on content type
  565. if filename.blank? && headers_store['Content-Type'].present? && headers_store['Content-Type'].match?(%r{^message/rfc822}i)
  566. begin
  567. parser = Channel::EmailParser.new
  568. mail_local = parser.parse(file.body.to_s)
  569. filename = if mail_local[:subject].present?
  570. "#{mail_local[:subject]}.eml"
  571. elsif headers_store['Content-Description'].present?
  572. "#{headers_store['Content-Description']}.eml".to_s.force_encoding('utf-8')
  573. else
  574. 'Mail.eml'
  575. end
  576. rescue
  577. filename = 'Mail.eml'
  578. end
  579. end
  580. # e. g. Content-Type: video/quicktime; name="Video.MOV";
  581. if filename.blank?
  582. ['(filename|name)(\*{0,1})="(.+?)"(;|$)', '(filename|name)(\*{0,1})=\'(.+?)\'(;|$)', '(filename|name)(\*{0,1})=(.+?)(;|$)'].each do |regexp|
  583. if headers_store['Content-Type'] =~ /#{regexp}/i
  584. filename = $3
  585. break
  586. end
  587. end
  588. end
  589. # workaround for mail gem - decode filenames
  590. # https://github.com/zammad/zammad/issues/928
  591. if filename.present?
  592. filename = Mail::Encodings.value_decode(filename)
  593. end
  594. if !filename.force_encoding('UTF-8').valid_encoding?
  595. filename = filename.utf8_encode(fallback: :read_as_sanitized_binary)
  596. end
  597. # e. g. Content-Type: video/quicktime
  598. if filename.blank? && (content_type = headers_store['Content-Type'])
  599. map = {
  600. 'message/delivery-status': %w[txt delivery-status],
  601. 'text/plain': %w[txt document],
  602. 'text/html': %w[html document],
  603. 'video/quicktime': %w[mov video],
  604. 'image/jpeg': %w[jpg image],
  605. 'image/jpg': %w[jpg image],
  606. 'image/png': %w[png image],
  607. 'image/gif': %w[gif image],
  608. }
  609. map.each do |type, ext|
  610. next if !content_type.match?(/^#{Regexp.quote(type)}/i)
  611. filename = if headers_store['Content-Description'].present?
  612. "#{headers_store['Content-Description']}.#{ext[0]}".to_s.force_encoding('utf-8')
  613. else
  614. "#{ext[1]}.#{ext[0]}"
  615. end
  616. break
  617. end
  618. end
  619. # set fallback filename
  620. if filename.blank?
  621. filename = 'file'
  622. end
  623. # create uniq filename
  624. local_filename = ''
  625. local_extention = ''
  626. if filename =~ /^(.*?)\.(.+?)$/
  627. local_filename = $1
  628. local_extention = $2
  629. end
  630. 1.upto(1000) do |i|
  631. filename_exists = false
  632. attachments.each do |attachment|
  633. if attachment[:filename] == filename
  634. filename_exists = true
  635. end
  636. end
  637. break if filename_exists == false
  638. filename = if local_extention.present?
  639. "#{local_filename}#{i}.#{local_extention}"
  640. else
  641. "#{local_filename}#{i}"
  642. end
  643. end
  644. # get mime type
  645. if file.header[:content_type]&.string
  646. headers_store['Mime-Type'] = file.header[:content_type].string
  647. end
  648. # get charset
  649. if file.header&.charset
  650. headers_store['Charset'] = file.header.charset
  651. end
  652. # remove not needed header
  653. headers_store.delete('Content-Transfer-Encoding')
  654. headers_store.delete('Content-Disposition')
  655. attach = {
  656. data: file.body.to_s,
  657. filename: filename,
  658. preferences: headers_store,
  659. }
  660. [attach]
  661. end
  662. # Archive the given message as tmp/folder/md5.eml
  663. def archive_mail(folder, msg)
  664. path = Rails.root.join('tmp', folder)
  665. FileUtils.mkpath path
  666. # MD5 hash the msg and save it as "md5.eml"
  667. md5 = Digest::MD5.hexdigest(msg)
  668. file_path = Rails.root.join('tmp', folder, "#{md5}.eml")
  669. File.open(file_path, 'wb') do |file|
  670. file.write msg
  671. end
  672. file_path
  673. end
  674. # Auto reply as the postmaster to oversized emails with:
  675. # [undeliverable] Message too large
  676. def postmaster_response(channel, msg)
  677. begin
  678. reply_mail = compose_postmaster_reply(msg)
  679. rescue NotificationFactory::FileNotFoundError => e
  680. Rails.logger.error 'No valid postmaster email_oversized template found. Skipping postmaster reply. ' + e.inspect
  681. return
  682. end
  683. Rails.logger.error "Send mail too large postmaster message to: #{reply_mail[:to]}"
  684. reply_mail[:from] = EmailAddress.find_by(channel: channel).email
  685. channel.deliver(reply_mail)
  686. rescue => e
  687. Rails.logger.error "Error during sending of postmaster oversized email auto-reply: #{e.inspect}\n#{e.backtrace}"
  688. end
  689. # Compose a "Message too large" reply to the given message
  690. def compose_postmaster_reply(raw_incoming_mail, locale = nil)
  691. parsed_incoming_mail = Channel::EmailParser.new.parse(raw_incoming_mail)
  692. # construct a dummy mail object
  693. mail = OpenStruct.new
  694. mail.from_display_name = parsed_incoming_mail[:from_display_name]
  695. mail.subject = parsed_incoming_mail[:subject]
  696. mail.msg_size = format('%<MB>.2f', MB: raw_incoming_mail.size.to_f / 1024 / 1024)
  697. reply = NotificationFactory::Mailer.template(
  698. template: 'email_oversized',
  699. locale: locale,
  700. format: 'txt',
  701. objects: {
  702. mail: mail,
  703. },
  704. raw: true, # will not add application template
  705. standalone: true, # default: false - will send header & footer
  706. )
  707. reply.merge(
  708. to: parsed_incoming_mail[:from_email],
  709. body: reply[:body].gsub(/\n/, "\r\n"),
  710. content_type: 'text/plain',
  711. References: parsed_incoming_mail[:message_id],
  712. 'In-Reply-To': parsed_incoming_mail[:message_id],
  713. )
  714. end
  715. end
  716. module Mail
  717. # workaround to get content of no parseable headers - in most cases with non 7 bit ascii signs
  718. class Field
  719. def raw_value
  720. begin
  721. value = @raw_value.try(:utf8_encode)
  722. rescue
  723. value = @raw_value.utf8_encode(fallback: :read_as_sanitized_binary)
  724. end
  725. return value if value.blank?
  726. value.sub(/^.+?:(\s|)/, '')
  727. end
  728. end
  729. # issue#348 - IMAP mail fetching stops because of broken spam email (e. g. broken Content-Transfer-Encoding value see test/fixtures/mail43.box)
  730. # https://github.com/zammad/zammad/issues/348
  731. class Body
  732. def decoded
  733. if !Encodings.defined?(encoding)
  734. #raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
  735. Rails.logger.info "UnknownEncodingType: Don't know how to decode #{encoding}!"
  736. raw_source
  737. else
  738. Encodings.get_encoding(encoding).decode(raw_source)
  739. end
  740. end
  741. end
  742. end