You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mime.lua 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927
  1. --[[
  2. Copyright (c) 2018, Vsevolod Stakhov <vsevolod@highsecure.ru>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. local argparse = require "argparse"
  14. local ansicolors = require "ansicolors"
  15. local rspamd_util = require "rspamd_util"
  16. local rspamd_task = require "rspamd_task"
  17. local rspamd_logger = require "rspamd_logger"
  18. local lua_meta = require "lua_meta"
  19. local rspamd_url = require "rspamd_url"
  20. local lua_util = require "lua_util"
  21. local lua_mime = require "lua_mime"
  22. local ucl = require "ucl"
  23. -- Define command line options
  24. local parser = argparse()
  25. :name "rspamadm mime"
  26. :description "Mime manipulations provided by Rspamd"
  27. :help_description_margin(30)
  28. :command_target("command")
  29. :require_command(true)
  30. parser:option "-c --config"
  31. :description "Path to config file"
  32. :argname("<cfg>")
  33. :default(rspamd_paths["CONFDIR"] .. "/" .. "rspamd.conf")
  34. parser:mutex(
  35. parser:flag "-j --json"
  36. :description "JSON output",
  37. parser:flag "-U --ucl"
  38. :description "UCL output",
  39. parser:flag "-M --messagepack"
  40. :description "MessagePack output"
  41. )
  42. parser:flag "-C --compact"
  43. :description "Use compact format"
  44. parser:flag "--no-file"
  45. :description "Do not print filename"
  46. -- Extract subcommand
  47. local extract = parser:command "extract ex e"
  48. :description "Extracts data from MIME messages"
  49. extract:argument "file"
  50. :description "File to process"
  51. :argname "<file>"
  52. :args "+"
  53. extract:flag "-t --text"
  54. :description "Extracts plain text data from a message"
  55. extract:flag "-H --html"
  56. :description "Extracts htm data from a message"
  57. extract:option "-o --output"
  58. :description "Output format ('raw', 'content', 'oneline', 'decoded', 'decoded_utf')"
  59. :argname("<type>")
  60. :convert {
  61. raw = "raw",
  62. content = "content",
  63. oneline = "content_oneline",
  64. decoded = "raw_parsed",
  65. decoded_utf = "raw_utf"
  66. }
  67. :default "content"
  68. extract:flag "-w --words"
  69. :description "Extracts words"
  70. extract:flag "-p --part"
  71. :description "Show part info"
  72. extract:flag "-s --structure"
  73. :description "Show structure info (e.g. HTML tags)"
  74. extract:flag "-i --invisible"
  75. :description "Show invisible content for HTML parts"
  76. extract:option "-F --words-format"
  77. :description "Words format ('stem', 'norm', 'raw', 'full')"
  78. :argname("<type>")
  79. :convert {
  80. stem = "stem",
  81. norm = "norm",
  82. raw = "raw",
  83. full = "full",
  84. }
  85. :default "stem"
  86. local stat = parser:command "stat st s"
  87. :description "Extracts statistical data from MIME messages"
  88. stat:argument "file"
  89. :description "File to process"
  90. :argname "<file>"
  91. :args "+"
  92. stat:mutex(
  93. stat:flag "-m --meta"
  94. :description "Lua metatokens",
  95. stat:flag "-b --bayes"
  96. :description "Bayes tokens",
  97. stat:flag "-F --fuzzy"
  98. :description "Fuzzy hashes"
  99. )
  100. stat:flag "-s --shingles"
  101. :description "Show shingles for fuzzy hashes"
  102. local urls = parser:command "urls url u"
  103. :description "Extracts URLs from MIME messages"
  104. urls:argument "file"
  105. :description "File to process"
  106. :argname "<file>"
  107. :args "+"
  108. urls:mutex(
  109. urls:flag "-t --tld"
  110. :description "Get TLDs only",
  111. urls:flag "-H --host"
  112. :description "Get hosts only",
  113. urls:flag "-f --full"
  114. :description "Show piecewise urls as processed by Rspamd"
  115. )
  116. urls:flag "-u --unique"
  117. :description "Print only unique urls"
  118. urls:flag "-s --sort"
  119. :description "Sort output"
  120. urls:flag "--count"
  121. :description "Print count of each printed element"
  122. urls:flag "-r --reverse"
  123. :description "Reverse sort order"
  124. local modify = parser:command "modify mod m"
  125. :description "Modifies MIME message"
  126. modify:argument "file"
  127. :description "File to process"
  128. :argname "<file>"
  129. :args "+"
  130. modify:option "-a --add-header"
  131. :description "Adds specific header"
  132. :argname "<header=value>"
  133. :count "*"
  134. modify:option "-r --remove-header"
  135. :description "Removes specific header (all occurrences)"
  136. :argname "<header>"
  137. :count "*"
  138. modify:option "-R --rewrite-header"
  139. :description "Rewrites specific header, uses Lua string.format pattern"
  140. :argname "<header=pattern>"
  141. :count "*"
  142. modify:option "-t --text-footer"
  143. :description "Adds footer to text/plain parts from a specific file"
  144. :argname "<file>"
  145. modify:option "-H --html-footer"
  146. :description "Adds footer to text/html parts from a specific file"
  147. :argname "<file>"
  148. local sign = parser:command "sign"
  149. :description "Performs DKIM signing"
  150. sign:argument "file"
  151. :description "File to process"
  152. :argname "<file>"
  153. :args "+"
  154. sign:option "-d --domain"
  155. :description "Use specific domain"
  156. :argname "<domain>"
  157. :count "1"
  158. sign:option "-s --selector"
  159. :description "Use specific selector"
  160. :argname "<selector>"
  161. :count "1"
  162. sign:option "-k --key"
  163. :description "Use specific key of file"
  164. :argname "<key>"
  165. :count "1"
  166. sign:option "-t type"
  167. :description "ARC or DKIM signing"
  168. :argname("<arc|dkim>")
  169. :convert {
  170. ['arc'] = 'arc',
  171. ['dkim'] = 'dkim',
  172. }
  173. :default 'dkim'
  174. sign:option "-o --output"
  175. :description "Output format"
  176. :argname("<message|signature>")
  177. :convert {
  178. ['message'] = 'message',
  179. ['signature'] = 'signature',
  180. }
  181. :default 'message'
  182. local dump = parser:command "dump"
  183. :description "Dumps a raw message in different formats"
  184. dump:argument "file"
  185. :description "File to process"
  186. :argname "<file>"
  187. :args "+"
  188. -- Duplicate format for convenience
  189. dump:mutex(
  190. parser:flag "-j --json"
  191. :description "JSON output",
  192. parser:flag "-U --ucl"
  193. :description "UCL output",
  194. parser:flag "-M --messagepack"
  195. :description "MessagePack output"
  196. )
  197. local function load_config(opts)
  198. local _r,err = rspamd_config:load_ucl(opts['config'])
  199. if not _r then
  200. rspamd_logger.errx('cannot parse %s: %s', opts['config'], err)
  201. os.exit(1)
  202. end
  203. _r,err = rspamd_config:parse_rcl({'logging', 'worker'})
  204. if not _r then
  205. rspamd_logger.errx('cannot process %s: %s', opts['config'], err)
  206. os.exit(1)
  207. end
  208. end
  209. local function load_task(opts, fname)
  210. if not fname then
  211. fname = '-'
  212. end
  213. local res,task = rspamd_task.load_from_file(fname, rspamd_config)
  214. if not res then
  215. parser:error(string.format('cannot read message from %s: %s', fname,
  216. task))
  217. end
  218. if not task:process_message() then
  219. parser:error(string.format('cannot read message from %s: %s', fname,
  220. 'failed to parse'))
  221. end
  222. return task
  223. end
  224. local function highlight(fmt, ...)
  225. return ansicolors.white .. string.format(fmt, ...) .. ansicolors.reset
  226. end
  227. local function maybe_print_fname(opts, fname)
  228. if not opts.json and not opts['no-file'] then
  229. rspamd_logger.messagex(highlight('File: %s', fname))
  230. end
  231. end
  232. local function output_fmt(opts)
  233. local fmt = 'json'
  234. if opts.compact then fmt = 'json-compact' end
  235. if opts.ucl then fmt = 'ucl' end
  236. if opts.messagepack then fmt = 'msgpack' end
  237. return fmt
  238. end
  239. -- Print elements in form
  240. -- filename -> table of elements
  241. local function print_elts(elts, opts, func)
  242. local fun = require "fun"
  243. if opts.json or opts.ucl then
  244. io.write(ucl.to_format(elts, output_fmt(opts)))
  245. else
  246. fun.each(function(fname, elt)
  247. if not opts.json and not opts.ucl then
  248. if func then
  249. elt = fun.map(func, elt)
  250. end
  251. maybe_print_fname(opts, fname)
  252. fun.each(function(e)
  253. io.write(e)
  254. io.write("\n")
  255. end, elt)
  256. end
  257. end, elts)
  258. end
  259. end
  260. local function extract_handler(opts)
  261. local out_elts = {}
  262. local tasks = {}
  263. local process_func
  264. if opts.words then
  265. -- Enable stemming and urls detection
  266. load_config(opts)
  267. rspamd_url.init(rspamd_config:get_tld_path())
  268. rspamd_config:init_subsystem('langdet')
  269. end
  270. local function maybe_print_text_part_info(part, out)
  271. local fun = require "fun"
  272. if opts.part then
  273. local t = 'plain text'
  274. if part:is_html() then
  275. t = 'html'
  276. end
  277. if not opts.json and not opts.ucl then
  278. table.insert(out,
  279. rspamd_logger.slog('Part: %s: %s, language: %s, size: %s (%s raw), words: %s',
  280. part:get_mimepart():get_digest():sub(1,8),
  281. t,
  282. part:get_language(),
  283. part:get_length(), part:get_raw_length(),
  284. part:get_words_count()))
  285. table.insert(out,
  286. rspamd_logger.slog('Stats: %s',
  287. fun.foldl(function(acc, k, v)
  288. if acc ~= '' then
  289. return string.format('%s, %s:%s', acc, k, v)
  290. else
  291. return string.format('%s:%s', k,v)
  292. end
  293. end, '', part:get_stats())))
  294. end
  295. end
  296. end
  297. local function maybe_print_mime_part_info(part, out)
  298. if opts.part then
  299. if not opts.json and not opts.ucl then
  300. local mtype,msubtype = part:get_type()
  301. local det_mtype,det_msubtype = part:get_detected_type()
  302. table.insert(out,
  303. rspamd_logger.slog('Mime Part: %s: %s/%s (%s/%s detected), filename: %s (%s detected ext), size: %s',
  304. part:get_digest():sub(1,8),
  305. mtype, msubtype,
  306. det_mtype, det_msubtype,
  307. part:get_filename(),
  308. part:get_detected_ext(),
  309. part:get_length()))
  310. end
  311. end
  312. end
  313. local function print_words(words, full)
  314. local fun = require "fun"
  315. if not full then
  316. return table.concat(words, ' ')
  317. else
  318. return table.concat(
  319. fun.totable(
  320. fun.map(function(w)
  321. -- [1] - stemmed word
  322. -- [2] - normalised word
  323. -- [3] - raw word
  324. -- [4] - flags (table of strings)
  325. return string.format('%s|%s|%s(%s)',
  326. w[3], w[2], w[1], table.concat(w[4], ','))
  327. end, words)
  328. ),
  329. ' '
  330. )
  331. end
  332. end
  333. for _,fname in ipairs(opts.file) do
  334. local task = load_task(opts, fname)
  335. out_elts[fname] = {}
  336. if not opts.text and not opts.html then
  337. opts.text = true
  338. opts.html = true
  339. end
  340. if opts.words then
  341. local how_words = opts['words_format'] or 'stem'
  342. table.insert(out_elts[fname], 'meta_words: ' ..
  343. print_words(task:get_meta_words(how_words), how_words == 'full'))
  344. end
  345. if opts.text or opts.html then
  346. local mp = task:get_parts() or {}
  347. for _,mime_part in ipairs(mp) do
  348. local how = opts.output
  349. local part
  350. if mime_part:is_text() then part = mime_part:get_text() end
  351. if part and opts.text and not part:is_html() then
  352. maybe_print_text_part_info(part, out_elts[fname])
  353. maybe_print_mime_part_info(mime_part, out_elts[fname])
  354. if not opts.json and not opts.ucl then
  355. table.insert(out_elts[fname], '\n')
  356. end
  357. if opts.words then
  358. local how_words = opts['words_format'] or 'stem'
  359. table.insert(out_elts[fname], print_words(part:get_words(how_words),
  360. how_words == 'full'))
  361. else
  362. table.insert(out_elts[fname], tostring(part:get_content(how)))
  363. end
  364. elseif part and opts.html and part:is_html() then
  365. maybe_print_text_part_info(part, out_elts[fname])
  366. maybe_print_mime_part_info(mime_part, out_elts[fname])
  367. if not opts.json and not opts.ucl then
  368. table.insert(out_elts[fname], '\n')
  369. end
  370. if opts.words then
  371. local how_words = opts['words_format'] or 'stem'
  372. table.insert(out_elts[fname], print_words(part:get_words(how_words),
  373. how_words == 'full'))
  374. else
  375. if opts.structure then
  376. local hc = part:get_html()
  377. local res = {}
  378. process_func = function(elt)
  379. local fun = require "fun"
  380. if type(elt) == 'table' then
  381. return table.concat(fun.totable(
  382. fun.map(
  383. function(t)
  384. return rspamd_logger.slog("%s", t)
  385. end,
  386. elt)), '\n')
  387. else
  388. return rspamd_logger.slog("%s", elt)
  389. end
  390. end
  391. hc:foreach_tag('any', function(tag)
  392. local elt = {}
  393. local ex = tag:get_extra()
  394. elt.tag = tag:get_type()
  395. if ex then
  396. elt.extra = ex
  397. end
  398. local content = tag:get_content()
  399. if content then
  400. elt.content = tostring(content)
  401. end
  402. local style = tag:get_style()
  403. if style then
  404. elt.style = style
  405. end
  406. table.insert(res, elt)
  407. end)
  408. table.insert(out_elts[fname], res)
  409. else -- opts.structure
  410. table.insert(out_elts[fname], tostring(part:get_content(how)))
  411. end
  412. if opts.invisible then
  413. local hc = part:get_html()
  414. table.insert(out_elts[fname], string.format('invisible content: %s',
  415. tostring(hc:get_invisible())))
  416. end
  417. end
  418. end
  419. if not part then
  420. maybe_print_mime_part_info(mime_part, out_elts[fname])
  421. end
  422. end
  423. end
  424. table.insert(out_elts[fname], "")
  425. table.insert(tasks, task)
  426. end
  427. print_elts(out_elts, opts, process_func)
  428. -- To avoid use after free we postpone tasks destruction
  429. for _,task in ipairs(tasks) do task:destroy() end
  430. end
  431. local function stat_handler(opts)
  432. local fun = require "fun"
  433. local out_elts = {}
  434. load_config(opts)
  435. rspamd_url.init(rspamd_config:get_tld_path())
  436. rspamd_config:init_subsystem('langdet,stat') -- Needed to gen stat tokens
  437. local process_func
  438. for _,fname in ipairs(opts.file) do
  439. local task = load_task(opts, fname)
  440. out_elts[fname] = {}
  441. if opts.meta then
  442. local mt = lua_meta.gen_metatokens_table(task)
  443. out_elts[fname] = mt
  444. process_func = function(k, v)
  445. return string.format("%s = %s", k, v)
  446. end
  447. elseif opts.bayes then
  448. local bt = task:get_stat_tokens()
  449. out_elts[fname] = bt
  450. process_func = function(e)
  451. return string.format('%s (%d): "%s"+"%s", [%s]', e.data, e.win, e.t1 or "",
  452. e.t2 or "", table.concat(fun.totable(
  453. fun.map(function(k) return k end, e.flags)), ","))
  454. end
  455. elseif opts.fuzzy then
  456. local parts = task:get_parts() or {}
  457. out_elts[fname] = {}
  458. process_func = function(e)
  459. local ret = string.format('part: %s(%s): %s', e.type, e.file or "", e.digest)
  460. if opts.shingles and e.shingles then
  461. local sgl = {}
  462. for _,s in ipairs(e.shingles) do
  463. table.insert(sgl, string.format('%s: %s+%s+%s', s[1], s[2], s[3], s[4]))
  464. end
  465. ret = ret .. '\n' .. table.concat(sgl, '\n')
  466. end
  467. return ret
  468. end
  469. for _,part in ipairs(parts) do
  470. if not part:is_multipart() then
  471. local text = part:get_text()
  472. if text then
  473. local digest,shingles = text:get_fuzzy_hashes(task:get_mempool())
  474. table.insert(out_elts[fname], {
  475. digest = digest,
  476. shingles = shingles,
  477. type = string.format('%s/%s',
  478. ({part:get_type()})[1],
  479. ({part:get_type()})[2])
  480. })
  481. else
  482. table.insert(out_elts[fname], {
  483. digest = part:get_digest(),
  484. file = part:get_filename(),
  485. type = string.format('%s/%s',
  486. ({part:get_type()})[1],
  487. ({part:get_type()})[2])
  488. })
  489. end
  490. end
  491. end
  492. end
  493. task:destroy() -- No automatic dtor
  494. end
  495. print_elts(out_elts, opts, process_func)
  496. end
  497. local function urls_handler(opts)
  498. load_config(opts)
  499. rspamd_url.init(rspamd_config:get_tld_path())
  500. local out_elts = {}
  501. if opts.json then rspamd_logger.messagex('[') end
  502. for _,fname in ipairs(opts.file) do
  503. out_elts[fname] = {}
  504. local task = load_task(opts, fname)
  505. local elts = {}
  506. local function process_url(u)
  507. local s
  508. if opts.tld then
  509. s = u:get_tld()
  510. elseif opts.host then
  511. s = u:get_host()
  512. elseif opts.full then
  513. s = rspamd_logger.slog('%s: %s', u:get_text(), u:to_table())
  514. else
  515. s = u:get_text()
  516. end
  517. if opts.unique then
  518. if elts[s] then
  519. elts[s].count = elts[s].count + 1
  520. else
  521. elts[s] = {
  522. count = 1,
  523. url = u:to_table()
  524. }
  525. end
  526. else
  527. if opts.json then
  528. table.insert(elts, u)
  529. else
  530. table.insert(elts, s)
  531. end
  532. end
  533. end
  534. for _,u in ipairs(task:get_urls(true)) do
  535. process_url(u)
  536. end
  537. local json_elts = {}
  538. local function process_elt(s, u)
  539. if opts.unique then
  540. -- s is string, u is {url = url, count = count }
  541. if not opts.json then
  542. if opts.count then
  543. table.insert(json_elts, string.format('%s : %s', s, u.count))
  544. else
  545. table.insert(json_elts, s)
  546. end
  547. else
  548. local tb = u.url
  549. tb.count = u.count
  550. table.insert(json_elts, tb)
  551. end
  552. else
  553. -- s is index, u is url or string
  554. if opts.json then
  555. table.insert(json_elts, u)
  556. else
  557. table.insert(json_elts, u)
  558. end
  559. end
  560. end
  561. if opts.sort then
  562. local sfunc
  563. if opts.unique then
  564. sfunc = function(t, a, b)
  565. if t[a].count ~= t[b].count then
  566. if opts.reverse then
  567. return t[a].count > t[b].count
  568. else
  569. return t[a].count < t[b].count
  570. end
  571. else
  572. -- Sort lexicography
  573. if opts.reverse then
  574. return a > b
  575. else
  576. return a < b
  577. end
  578. end
  579. end
  580. else
  581. sfunc = function(t, a, b)
  582. local va, vb
  583. if opts.json then
  584. va = t[a]:get_text()
  585. vb = t[b]:get_text()
  586. else
  587. va = t[a]
  588. vb = t[b]
  589. end
  590. if opts.reverse then
  591. return va > vb
  592. else
  593. return va < vb
  594. end
  595. end
  596. end
  597. for s,u in lua_util.spairs(elts, sfunc) do
  598. process_elt(s, u)
  599. end
  600. else
  601. for s,u in pairs(elts) do
  602. process_elt(s, u)
  603. end
  604. end
  605. out_elts[fname] = json_elts
  606. task:destroy() -- No automatic dtor
  607. end
  608. print_elts(out_elts, opts)
  609. end
  610. local function newline(task)
  611. local t = task:get_newlines_type()
  612. if t == 'cr' then
  613. return '\r'
  614. elseif t == 'lf' then
  615. return '\n'
  616. end
  617. return '\r\n'
  618. end
  619. local function modify_handler(opts)
  620. load_config(opts)
  621. rspamd_url.init(rspamd_config:get_tld_path())
  622. local function read_file(file)
  623. local f = assert(io.open(file, "rb"))
  624. local content = f:read("*all")
  625. f:close()
  626. return content
  627. end
  628. local text_footer, html_footer
  629. if opts['text_footer'] then
  630. text_footer = read_file(opts['text_footer'])
  631. end
  632. if opts['html_footer'] then
  633. html_footer = read_file(opts['html_footer'])
  634. end
  635. for _,fname in ipairs(opts.file) do
  636. local task = load_task(opts, fname)
  637. local newline_s = newline(task)
  638. local seen_cte
  639. local rewrite = lua_mime.add_text_footer(task, html_footer, text_footer) or {}
  640. local out = {} -- Start with headers
  641. local function process_headers_cb(name, hdr)
  642. for _,h in ipairs(opts['remove_header']) do
  643. if name:match(h) then
  644. return
  645. end
  646. end
  647. for _,h in ipairs(opts['rewrite_header']) do
  648. local hname,hpattern = h:match('^([^=]+)=(.+)$')
  649. if hname == name then
  650. local new_value = string.format(hpattern, hdr.decoded)
  651. new_value = string.format('%s:%s%s',
  652. name, hdr.separator,
  653. rspamd_util.fold_header(name,
  654. rspamd_util.mime_header_encode(new_value),
  655. task:get_newlines_type()))
  656. out[#out + 1] = new_value
  657. return
  658. end
  659. end
  660. if rewrite.need_rewrite_ct then
  661. if name:lower() == 'content-type' then
  662. local nct = string.format('%s: %s/%s; charset=utf-8',
  663. 'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
  664. out[#out + 1] = nct
  665. return
  666. elseif name:lower() == 'content-transfer-encoding' then
  667. out[#out + 1] = string.format('%s: %s',
  668. 'Content-Transfer-Encoding', rewrite.new_cte or 'quoted-printable')
  669. seen_cte = true
  670. return
  671. end
  672. end
  673. out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
  674. end
  675. task:headers_foreach(process_headers_cb, {full = true})
  676. for _,h in ipairs(opts['add_header']) do
  677. local hname,hvalue = h:match('^([^=]+)=(.+)$')
  678. if hname and hvalue then
  679. out[#out + 1] = string.format('%s: %s', hname,
  680. rspamd_util.fold_header(hname, hvalue, task:get_newlines_type()))
  681. end
  682. end
  683. if not seen_cte and rewrite.need_rewrite_ct then
  684. out[#out + 1] = string.format('%s: %s',
  685. 'Content-Transfer-Encoding', rewrite.new_cte or 'quoted-printable')
  686. end
  687. -- End of headers
  688. out[#out + 1] = ''
  689. if rewrite.out then
  690. for _,o in ipairs(rewrite.out) do
  691. out[#out + 1] = o
  692. end
  693. else
  694. out[#out + 1] = {task:get_rawbody(), false}
  695. end
  696. for _,o in ipairs(out) do
  697. if type(o) == 'string' then
  698. io.write(o)
  699. io.write(newline_s)
  700. elseif type(o) == 'table' then
  701. io.flush()
  702. if type(o[1]) == 'string' then
  703. io.write(o[1])
  704. else
  705. o[1]:save_in_file(1)
  706. end
  707. if o[2] then
  708. io.write(newline_s)
  709. end
  710. else
  711. o:save_in_file(1)
  712. io.write(newline_s)
  713. end
  714. end
  715. task:destroy() -- No automatic dtor
  716. end
  717. end
  718. local function sign_handler(opts)
  719. load_config(opts)
  720. rspamd_url.init(rspamd_config:get_tld_path())
  721. local lua_dkim = require("lua_ffi").dkim
  722. if not lua_dkim then
  723. io.stderr:write('FFI support is required: please use LuaJIT or install lua-ffi')
  724. os.exit(1)
  725. end
  726. local sign_key
  727. if rspamd_util.file_exists(opts.key) then
  728. sign_key = lua_dkim.load_sign_key(opts.key, 'file')
  729. else
  730. sign_key = lua_dkim.load_sign_key(opts.key, 'base64')
  731. end
  732. if not sign_key then
  733. io.stderr:write('Cannot load key: ' .. opts.key .. '\n')
  734. os.exit(1)
  735. end
  736. for _,fname in ipairs(opts.file) do
  737. local task = load_task(opts, fname)
  738. local ctx = lua_dkim.create_sign_context(task, sign_key, nil, opts.algorithm)
  739. if not ctx then
  740. io.stderr:write('Cannot init signing\n')
  741. os.exit(1)
  742. end
  743. local sig = lua_dkim.do_sign(task, ctx, opts.selector, opts.domain)
  744. if not sig then
  745. io.stderr:write('Cannot create signature\n')
  746. os.exit(1)
  747. end
  748. if opts.output == 'signature' then
  749. io.write(sig)
  750. io.write('\n')
  751. io.flush()
  752. else
  753. local dkim_hdr = string.format('%s: %s%s',
  754. 'DKIM-Signature',
  755. rspamd_util.fold_header('DKIM-Signature',
  756. rspamd_util.mime_header_encode(sig),
  757. task:get_newlines_type()),
  758. newline(task))
  759. io.write(dkim_hdr)
  760. io.flush()
  761. task:get_content():save_in_file(1)
  762. end
  763. task:destroy() -- No automatic dtor
  764. end
  765. end
  766. local function dump_handler(opts)
  767. load_config(opts)
  768. rspamd_url.init(rspamd_config:get_tld_path())
  769. for _,fname in ipairs(opts.file) do
  770. local task = load_task(opts, fname)
  771. if opts.ucl or opts.json or opts.messagepack then
  772. local ucl_object = lua_mime.message_to_ucl(task)
  773. io.write(ucl.to_format(ucl_object, output_fmt(opts)))
  774. else
  775. task:get_content():save_in_file(1)
  776. end
  777. task:destroy() -- No automatic dtor
  778. end
  779. end
  780. local function handler(args)
  781. local opts = parser:parse(args)
  782. local command = opts.command
  783. if type(opts.file) == 'string' then
  784. opts.file = {opts.file}
  785. elseif type(opts.file) == 'none' then
  786. opts.file = {}
  787. end
  788. if command == 'extract' then
  789. extract_handler(opts)
  790. elseif command == 'stat' then
  791. stat_handler(opts)
  792. elseif command == 'urls' then
  793. urls_handler(opts)
  794. elseif command == 'modify' then
  795. modify_handler(opts)
  796. elseif command == 'sign' then
  797. sign_handler(opts)
  798. elseif command == 'dump' then
  799. dump_handler(opts)
  800. else
  801. parser:error('command %s is not implemented', command)
  802. end
  803. end
  804. return {
  805. name = 'mime',
  806. aliases = {'mime_tool'},
  807. handler = handler,
  808. description = parser._description
  809. }