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

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