email_parser.rb 28 KB

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