email_parser.rb 28 KB

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