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 22KB

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