No Description
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.

smd-client 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. #! /usr/bin/env lua5.1
  2. --
  3. -- Released under the terms of GPLv3 or at your option any later version.
  4. -- No warranties.
  5. -- Copyright Enrico Tassi <gares@fettunta.org>
  6. require 'syncmaildir'
  7. -- export syncmaildir to the global namespace
  8. for k,v in pairs(syncmaildir) do _G[k] = v end
  9. -- globals counter for statistics
  10. local statistics = {
  11. added = 0,
  12. removed = 0,
  13. received = 0,
  14. xdelta = 0,
  15. files = {},
  16. }
  17. -- ========================= get mail queue =================================
  18. -- queue for fetching mails in blocks of queue_max_len messages
  19. -- to cope with latency
  20. local get_full_email_queue = {}
  21. local queue_max_len = 50
  22. function process_get_full_email_queue()
  23. local command = {}
  24. for _,v in ipairs(get_full_email_queue) do
  25. command[#command+1] = 'GET ' .. v.name
  26. end
  27. command[#command+1] = ''
  28. io.write(table.concat(command,'\n'))
  29. command = nil
  30. io.flush()
  31. local tmp = {}
  32. for _,v in ipairs(get_full_email_queue) do
  33. tmp[#tmp+1] = tmp_for(v.name)
  34. v.tmp = tmp[#tmp]
  35. statistics.received = statistics.received + receive(io.stdin, tmp[#tmp])
  36. end
  37. tmp = nil
  38. for _,v in ipairs(get_full_email_queue) do
  39. local hsha_l, bsha_l = sha_file(v.tmp)
  40. if hsha_l == v.hsha and bsha_l == v.bsha then
  41. local rc = os.rename(v.tmp, v.name)
  42. if rc then
  43. statistics.added = statistics.added + 1
  44. else
  45. log_error('Failed to rename '..v.tmp..' to '..v.name)
  46. log_error('It may be caused by bad directory permissions, '..
  47. 'please check.')
  48. os.remove(v.tmp)
  49. return (trace(false)) -- fail rename tmpfile to actual name
  50. end
  51. else
  52. log_error('The server sent a different email for '..v.name)
  53. log_error('This problem should be transient, please retry.')
  54. os.remove(v.tmp)
  55. return (trace(false)) -- get full email failed, received wrong mail
  56. end
  57. end
  58. get_full_email_queue = {}
  59. return (trace(true)) -- get full email OK
  60. end
  61. function process_pending_queue()
  62. local rc = process_get_full_email_queue()
  63. if not rc then
  64. io.write('ABORT\n')
  65. io.flush()
  66. os.exit(1)
  67. end
  68. end
  69. -- the function to fetch a mail message
  70. function get_full_email(name,hsha,bsha)
  71. if dry_run() then
  72. statistics.added = statistics.added + 1
  73. statistics.files[#statistics.files + 1] = name
  74. return true
  75. end
  76. get_full_email_queue[#get_full_email_queue+1] = {
  77. name = name;
  78. hsha = hsha;
  79. bsha = bsha;
  80. }
  81. return true
  82. end
  83. -- ======================== header replacing =================================
  84. function merge_mail(header,body,target)
  85. local h = io.open(header,"r")
  86. local b = io.open(body,"r")
  87. local t = io.open(target,"w")
  88. local l
  89. while true do
  90. l = h:read("*l")
  91. if l and l ~= "" then t:write(l,'\n') else break end
  92. end
  93. while true do
  94. l = b:read("*l")
  95. if not l or l == "" then break end
  96. end
  97. t:write('\n')
  98. while true do
  99. l = b:read("*l")
  100. if l then t:write(l,'\n') else break end
  101. end
  102. h:close()
  103. b:close()
  104. t:close()
  105. end
  106. function get_header_and_merge(name,hsha)
  107. local tmpfile = tmp_for(name)
  108. io.write('GETHEADER '..name..'\n')
  109. io.flush()
  110. receive(io.stdin, tmpfile)
  111. local hsha_l, _ = sha_file(tmpfile)
  112. if hsha_l == hsha then
  113. if not dry_run() then
  114. local tmpfile1 = tmp_for(name)
  115. merge_mail(tmpfile,name,tmpfile1)
  116. os.remove(tmpfile)
  117. os.rename(tmpfile1, name)
  118. else
  119. -- we delete the new piece without merging (--dry-run)
  120. os.remove(tmpfile)
  121. end
  122. return (trace(true)) -- get header OK
  123. else
  124. os.remove(tmpfile)
  125. log_error('The server sent a different email header for '..name)
  126. log_error('This problem should be transient, please retry.')
  127. log_tags("receive-header","modify-while-update",false,"retry")
  128. return (trace(false)) -- get header fails, got a different header
  129. end
  130. end
  131. -- ============================= renaming ====================================
  132. function compute_renamings(actions)
  133. local copy = {}
  134. local delete = {}
  135. local script = {}
  136. for _, cmd in ipairs(actions) do
  137. local opcode = parse(cmd, '^(%S+)')
  138. if opcode == "COPY" then
  139. local name_src, hsha, bsha, name_tgt =
  140. parse(cmd, '^COPY (%S+) (%S+) (%S+) TO (%S+)$')
  141. name_src = url_decode(name_src)
  142. name_tgt = url_decode(name_tgt)
  143. copy[#copy + 1] = { src = name_src, tgt = name_tgt}
  144. elseif opcode == "DELETE" then
  145. local name, hsha, bsha = parse(cmd, '^DELETE (%S+) (%S+) (%S+)$')
  146. name = url_decode(name)
  147. delete[name] = 1
  148. elseif opcode == 'ERROR' then
  149. local msg = parse(cmd, '^ERROR (.*)$')
  150. execute_error(msg)
  151. io.write('ABORT\n')
  152. io.flush()
  153. os.exit(6)
  154. end
  155. end
  156. for _, cp in ipairs(copy) do
  157. -- it is a real move
  158. if delete[cp.src] then
  159. local absolute1, t1, last1 = tokenize_path(cp.src)
  160. local absolute2, t2, last2 = tokenize_path(cp.tgt)
  161. local dir1 = table.concat(t1,'/')
  162. local dir2 = table.concat(t2,'/')
  163. if not absolute1 and not absolute2 and
  164. last1 ~= nil and last2 ~= nil and
  165. (t1[#t1] == "cur" or t1[#t1] == "new") and
  166. (t2[#t2] == "cur" or t2[#t2] == "new") and
  167. dir1 == dir2 then
  168. --and is_translator_set() then
  169. local t_dir = homefy(translate(dir1))
  170. if delete[cp.src] > 1 then
  171. table.insert(script,1,string.format("cp %s %s",
  172. quote(t_dir..'/'..last1),
  173. quote(t_dir..'/'..last2)))
  174. else
  175. script[#script + 1] = string.format("mv %s %s",
  176. quote(t_dir..'/'..last1),
  177. quote(t_dir..'/'..last2))
  178. end
  179. delete[cp.src] = delete[cp.src] + 1
  180. end
  181. end
  182. end
  183. return script
  184. end
  185. -- ============================= actions =====================================
  186. function execute_add(name, hsha, bsha)
  187. local ex, hsha_l, bsha_l = exists_and_sha(name)
  188. if ex then
  189. if hsha == hsha_l and bsha == bsha_l then
  190. return (trace(true)) -- skipping add since already there
  191. else
  192. log_error('Failed to add '..name..
  193. ' since a file with the same name')
  194. log_error('exists but its content is different.')
  195. log_error('To fix this problem you should rename '..name)
  196. log_error('Executing `cd; mv -n '..quote(name)..' '..
  197. quote(tmp_for(name,false))..'` should work.')
  198. log_tags("mail-addition","concurrent-mailbox-edit",true,
  199. mk_act("mv", name))
  200. return (trace(false)) -- skipping add since already there but !=
  201. end
  202. end
  203. return (get_full_email(name,hsha,bsha))
  204. end
  205. function execute_delete(name, hsha, bsha)
  206. local ex, hsha_l, bsha_l = exists_and_sha(name)
  207. if ex then
  208. if hsha == hsha_l and bsha == bsha_l then
  209. local rc
  210. if not dry_run() then
  211. rc = os.remove(name)
  212. else
  213. rc = true -- we do not delete the message for real (--dry-run)
  214. end
  215. if rc then
  216. statistics.removed = statistics.removed + 1
  217. return (trace(true)) -- removed successfully
  218. else
  219. log_error('Deletion of '..name..' failed.')
  220. log_error('It may be caused by bad directory permissions, '..
  221. 'please check.')
  222. log_tags("delete-message","bad-directory-permission",true,
  223. mk_act("permission",name))
  224. return (trace(false)) -- os.remove failed
  225. end
  226. else
  227. log_error('Failed to delete '..name..
  228. ' since the local copy of it has')
  229. log_error('modifications.')
  230. log_error('To fix this problem you have two options:')
  231. log_error('- delete '..name..' by hand')
  232. log_error('- run @@INVERSECOMMAND@@ so that this file is added '..
  233. 'to the other mailbox')
  234. log_tags("delete-message", "concurrent-mailbox-edit",true,
  235. mk_act('display',name),
  236. mk_act('rm',name),
  237. "run(@@INVERSECOMMAND@@ @@ENDPOINT@@)")
  238. return (trace(false)) -- remove fails since local file is !=
  239. end
  240. end
  241. return (trace(true)) -- already removed
  242. end
  243. function execute_copy(name_src, hsha, bsha, name_tgt)
  244. local ex_src, hsha_src, bsha_src = exists_and_sha(name_src)
  245. local ex_tgt, hsha_tgt, bsha_tgt = exists_and_sha(name_tgt)
  246. if ex_src and ex_tgt then
  247. if hsha_src == hsha_tgt and bsha_src == bsha_tgt and
  248. hsha_src == hsha and bsha_src == bsha then
  249. return (trace(true)) -- skip copy, already there
  250. else
  251. log_error('Failed to copy '..name_src..' to '..name_tgt)
  252. log_error('The destination already exists but its content differs.')
  253. log_error('To fix this problem you have two options:')
  254. log_error('- rename '..name_tgt..' by hand so that '..name_src)
  255. log_error(' can be copied without replacing it.')
  256. log_error(' Executing `cd; mv -n '..quote(name_tgt)..' '..
  257. quote(tmp_for(name_tgt,false))..'` should work.')
  258. log_error('- run @@INVERSECOMMAND@@ so that your changes to '..
  259. name_tgt)
  260. log_error(' are propagated to the other mailbox')
  261. log_tags("copy-message","concurrent-mailbox-edit",true,
  262. mk_act('mv',name_tgt),
  263. "run(@@INVERSECOMMAND@@ @@ENDPOINT@@)")
  264. return (trace(false)) -- fail copy, already there but !=
  265. end
  266. elseif ex_src and not ex_tgt then
  267. if hsha_src == hsha and bsha_src == bsha then
  268. local ok, err
  269. if not dry_run() then
  270. ok, err = cp(name_src,name_tgt)
  271. else
  272. ok = 0 -- we do not copy for real (--dry-run)
  273. end
  274. if ok == 0 then
  275. return (trace(true)) -- copy successful
  276. else
  277. log_error('Failed to copy '..name_src..' to '..name_tgt..
  278. ' : '..(err or 'unknown error'))
  279. log_tags("delete-message","bad-directory-permission",true,
  280. mk_act('display', name_tgt))
  281. return (trace(false)) -- copy failed (cp command failed)
  282. end
  283. else
  284. -- sub-optimal, we may reuse body or header
  285. return (get_full_email(name_tgt,hsha,bsha))
  286. end
  287. elseif not ex_src and ex_tgt then
  288. if hsha == hsha_tgt and bsha == bsha_tgt then
  289. return (trace(true)) -- skip copy, already there (only the copy)
  290. else
  291. log_error('Failed to copy '..name_src..' to '..name_tgt)
  292. log_error('The source file has been locally removed.')
  293. log_error('The destination file already exists but its '..
  294. 'content differs.')
  295. log_error('To fix this problem you have two options:')
  296. log_error('- rename '..name_tgt..' by hand so that '..
  297. name_src..' can be')
  298. log_error(' copied without replacing it.')
  299. log_error(' Executing `cd; mv -n '..quote(name_tgt)..' '..
  300. quote(tmp_for(name_tgt,false))..'` should work.')
  301. log_error('- run @@INVERSECOMMAND@@ so that your changes to '..
  302. name_tgt..' are')
  303. log_error(' propagated to the other mailbox')
  304. log_tags("copy-message","concurrent-mailbox-edit",true,
  305. mk_act('mv', name_tgt),
  306. "run(@@INVERSECOMMAND@@ @@ENDPOINT@@)")
  307. return (trace(false)) -- skip copy, already there and !=, no source
  308. end
  309. else
  310. return (get_full_email(name_tgt,hsha,bsha))
  311. end
  312. end
  313. function execute_move(name_src, hsha, bsha, name_tgt)
  314. local ex_src, hsha_src, bsha_src = exists_and_sha(name_src)
  315. local ex_tgt, hsha_tgt, bsha_tgt = exists_and_sha(name_tgt)
  316. if ex_src and ex_tgt then
  317. if hsha_tgt == hsha and bsha_tgt == bsha then
  318. -- the target is already in place
  319. if hsha_src == hsha and bsha_src == bsha then
  320. return (execute_delete(name_src,hsha,bsha))
  321. else
  322. return (trace(true)) -- the source has changes, nothing to do
  323. end
  324. else
  325. log_error('Failed to move '..name_src..' to '..name_tgt)
  326. log_error('The destination already exists but its content differs.')
  327. log_error('To fix this problem you have two options:')
  328. log_error('- rename '..name_tgt..' by hand so that '..name_src)
  329. log_error(' can be copied without replacing it.')
  330. log_error(' Executing `cd; mv -n '..quote(name_tgt)..' '..
  331. quote(tmp_for(name_tgt,false))..'` should work.')
  332. log_error('- run @@INVERSECOMMAND@@ so that your changes to '..
  333. name_tgt)
  334. log_error(' are propagated to the other mailbox')
  335. log_tags("move-message","concurrent-mailbox-edit",true,
  336. mk_act('mv',name_tgt),
  337. "run(@@INVERSECOMMAND@@ @@ENDPOINT@@)")
  338. return (trace(false)) -- fail move, already there but !=
  339. end
  340. elseif ex_src and not ex_tgt then
  341. if hsha_src == hsha and bsha_src == bsha then
  342. local ok, err
  343. if not dry_run() then
  344. ok, err = os.rename(name_src,name_tgt)
  345. else
  346. ok = true -- we do not move for real (--dry-run)
  347. end
  348. if ok then
  349. return (trace(true)) -- move successful
  350. else
  351. log_error('Failed to move '..name_src..' to '..name_tgt..
  352. ' : '..(err or 'unknown error'))
  353. log_tags("move-message","bad-directory-permission",true,
  354. mk_act('display', name_tgt))
  355. return (trace(false)) -- copy failed (cp command failed)
  356. end
  357. else
  358. -- sub-optimal, we may reuse body or header
  359. return (get_full_email(name_tgt,hsha,bsha))
  360. end
  361. elseif not ex_src and ex_tgt then
  362. if hsha == hsha_tgt and bsha == bsha_tgt then
  363. return (trace(true)) -- skip move, already there (and no source)
  364. else
  365. log_error('Failed to move '..name_src..' to '..name_tgt)
  366. log_error('The source file has been locally removed.')
  367. log_error('The destination file already exists but its '..
  368. 'content differs.')
  369. log_error('To fix this problem you have two options:')
  370. log_error('- rename '..name_tgt..' by hand so that '..
  371. name_src..' can be')
  372. log_error(' copied without replacing it.')
  373. log_error(' Executing `cd; mv -n '..quote(name_tgt)..' '..
  374. quote(tmp_for(name_tgt,false))..'` should work.')
  375. log_error('- run @@INVERSECOMMAND@@ so that your changes to '..
  376. name_tgt..' are')
  377. log_error(' propagated to the other mailbox')
  378. log_tags("copy-message","concurrent-mailbox-edit",true,
  379. mk_act('mv', name_tgt),
  380. "run(@@INVERSECOMMAND@@ @@ENDPOINT@@)")
  381. return (trace(false)) -- skip copy, already there and !=, no source
  382. end
  383. else
  384. return (get_full_email(name_tgt,hsha,bsha))
  385. end
  386. end
  387. function execute_replaceheader(name, hsha, bsha, hsha_new)
  388. if exists(name) then
  389. local hsha_l, bsha_l = sha_file(name)
  390. if hsha == hsha_l and bsha == bsha_l then
  391. return (get_header_and_merge(name,hsha_new))
  392. elseif hsha_l == hsha_new and bsha == bsha_l then
  393. return (trace(true)) -- replace header ok, already changend
  394. else
  395. log_error('Failed to replace '..name..' header since it has local')
  396. log_error(' modifications.')
  397. log_error('To fix this problem you should rename '..name)
  398. log_error('Executing `cd; mv -n '..quote(name)..' '..
  399. quote(tmp_for(name,false))..'` should work.')
  400. log_tags("header-replacement","concurrent-mailbox-edit",true,
  401. mk_act('mv', name))
  402. return (trace(false)) -- replace header fails, local header !=
  403. end
  404. else
  405. return (get_full_email(name,hsha_new,bsha))
  406. end
  407. end
  408. function execute_copybody(name, bsha, newname, hsha)
  409. local exn, hsha_ln, bsha_ln = exists_and_sha(newname)
  410. if not exn then
  411. local ex, _, bsha_l = exists_and_sha(name)
  412. if ex and bsha_l == bsha then
  413. local ok, err
  414. if not dry_run() then
  415. ok, err = cp(name,newname)
  416. else
  417. ok = 0 -- we do not copy the body for merging (--dry-run)
  418. end
  419. if ok == 0 then
  420. ok = get_header_and_merge(newname,hsha)
  421. if ok then
  422. return (trace(true)) -- copybody OK
  423. else
  424. os.remove(newname)
  425. return (trace(false)) -- copybody failed, bad new header
  426. end
  427. else
  428. log_error('Failed to copy '..name..' to '..newname..' : '..
  429. (err or 'unknown error'))
  430. log_tags("copy-message","bad-directory-permission",true,
  431. mk_act('display', newname))
  432. return (trace(false)) -- copybody failed (cp command failed)
  433. end
  434. else
  435. return(get_full_email(newname,hsha,bsha))
  436. end
  437. else
  438. if bsha == bsha_ln and hsha == hsha_ln then
  439. return (trace(true)) -- copybody OK (already there)
  440. else
  441. log_error('Failed to copy body of '..name..' to '..newname)
  442. log_error('To fix this problem you should rename '..newname)
  443. log_error('Executing `cd; mv -n '..quote(newname)..' '..
  444. quote(tmp_for(newname,false))..'` should work.')
  445. log_tags("copy-body","concurrent-mailbox-edit",true,
  446. mk_act('mv', newname))
  447. return (trace(false)) -- copybody failed (already there, != )
  448. end
  449. end
  450. end
  451. function execute_replace(name1, hsha1, bsha1, hsha2, bsha2)
  452. local exn, hsha_ln, bsha_ln = exists_and_sha(name1)
  453. if not exn then
  454. return(get_full_email(name1,hsha2,bsha2))
  455. else
  456. if bsha2 == bsha_ln and hsha2 == hsha_ln then
  457. return (trace(true)) -- replace OK (already there)
  458. elseif bsha1 == bsha_ln and hsha1 == hsha_ln then
  459. return(get_full_email(name1,hsha2,bsha2))
  460. else
  461. log_error('Failed to replace '..name1)
  462. log_error('To fix this problem you should rename '..name1)
  463. log_error('Executing `cd; mv -n '..quote(name1)..' '..
  464. quote(tmp_for(name1,false))..'` should work.')
  465. log_tags("replace","concurrent-mailbox-edit",true,
  466. mk_act('mv', name1))
  467. return (trace(false)) -- replace failed (already there, != )
  468. end
  469. end
  470. end
  471. function execute_error(msg)
  472. log_error('mddiff failed: '..msg)
  473. if msg:match('^Unable to open directory') then
  474. log_tags("mddiff","directory-disappeared",false)
  475. else
  476. log_tags("mddiff","unknown",true)
  477. end
  478. return (trace(false)) -- mddiff error
  479. end
  480. -- the main switch, dispatching actions.
  481. -- extra parentheses around execute_* calls make it a non tail call,
  482. -- thus we get the stack frame print in case of error.
  483. function execute(cmd)
  484. local opcode = parse(cmd, '^(%S+)')
  485. if opcode == "ADD" then
  486. local name, hsha, bsha = parse(cmd, '^ADD (%S+) (%S+) (%S+)$')
  487. name = url_decode(name)
  488. mkdir_p(name)
  489. return (execute_add(name, hsha, bsha))
  490. end
  491. if opcode == "DELETE" then
  492. local name, hsha, bsha = parse(cmd, '^DELETE (%S+) (%S+) (%S+)$')
  493. name = url_decode(name)
  494. mkdir_p(name)
  495. return (execute_delete(name, hsha, bsha))
  496. end
  497. if opcode == "COPY" then
  498. local name_src, hsha, bsha, name_tgt =
  499. parse(cmd, '^COPY (%S+) (%S+) (%S+) TO (%S+)$')
  500. name_src = url_decode(name_src)
  501. name_tgt = url_decode(name_tgt)
  502. mkdir_p(name_src)
  503. mkdir_p(name_tgt)
  504. return (execute_copy(name_src, hsha, bsha, name_tgt))
  505. end
  506. if opcode == "MOVE" then
  507. local name_src, hsha, bsha, name_tgt =
  508. parse(cmd, '^MOVE (%S+) (%S+) (%S+) TO (%S+)$')
  509. name_src = url_decode(name_src)
  510. name_tgt = url_decode(name_tgt)
  511. mkdir_p(name_src)
  512. mkdir_p(name_tgt)
  513. return (execute_move(name_src, hsha, bsha, name_tgt))
  514. end
  515. if opcode == "REPLACEHEADER" then
  516. local name, hsha, bsha, hsha_new =
  517. parse(cmd, '^REPLACEHEADER (%S+) (%S+) (%S+) WITH (%S+)$')
  518. name = url_decode(name)
  519. mkdir_p(name)
  520. return (execute_replaceheader(name, hsha, bsha, hsha_new))
  521. end
  522. if opcode == "COPYBODY" then
  523. local name, bsha, newname, hsha =
  524. parse(cmd, '^COPYBODY (%S+) (%S+) TO (%S+) (%S+)$')
  525. name = url_decode(name)
  526. newname = url_decode(newname)
  527. mkdir_p(name)
  528. mkdir_p(newname)
  529. return (execute_copybody(name, bsha, newname, hsha))
  530. end
  531. if opcode == "REPLACE" then
  532. local name1, hsha1, bsha1, hsha2, bsha2 =
  533. parse(cmd, '^REPLACE (%S+) (%S+) (%S+) WITH (%S+) (%S+)$')
  534. name1 = url_decode(name1)
  535. mkdir_p(name1)
  536. return (execute_replace(name1, hsha1, bsha1, hsha2, bsha2))
  537. end
  538. if opcode == "ERROR" then
  539. local msg = parse(cmd, '^ERROR (.*)$')
  540. return (execute_error(msg))
  541. end
  542. log_internal_error_and_fail('Unknown opcode '..opcode, "protocol")
  543. end
  544. -- ============================= MAIN =====================================
  545. -- report every n mails
  546. local report_frequency = 5000
  547. -- receive a list of commands
  548. function receive_delta(inf, firsttime)
  549. local cmds = {}
  550. local line = ""
  551. log_progress('Phase 1: changes detection')
  552. if firsttime then
  553. log_progress([[
  554. This phase computes the SHA1 sum of all the emails in the remote
  555. mailbox.
  556. Depending on the size of the mailbox size and the speed of the hard
  557. drive, this operation may take a lot of time. After the first
  558. synchronization it will be much faster, since only new emails have
  559. to be scanned.
  560. On a cheap laptop it takes 10m to scan a 1G mailbox.]])
  561. end
  562. repeat
  563. line = inf:read("*l")
  564. if line and line ~= "END" then cmds[#cmds+1] = line end
  565. if #cmds % report_frequency == 0 and #cmds > 0 then
  566. log_progress(string.format(' %3dK emails scanned', #cmds / 1000))
  567. end
  568. until not line or line == "END"
  569. if line ~= "END" then
  570. log_error('Unable to receive a complete diff')
  571. log_tags_and_fail("network error while receiving delta",
  572. "receive-delta","network",false,"retry")
  573. end
  574. return cmds
  575. end
  576. function main()
  577. -- sanity checks for external softwares
  578. assert_exists(MDDIFF)
  579. assert_exists(XDELTA)
  580. -- argument parsing
  581. local usage = "Usage: "..arg[0]:match('[^/]+$')..
  582. " [-vd] [-t translatorRL] endpointname mailboxes...\n"
  583. local apply_xdelta = true
  584. local rename_only = false
  585. local override_db = nil
  586. while #arg > 2 do
  587. if arg[1] == '-v' or arg[1] == '--verbose' then
  588. set_verbose(true)
  589. table.remove(arg,1)
  590. elseif arg[1] == '-d' or arg[1] == '--dry-run' then
  591. set_dry_run(true)
  592. table.remove(arg,1)
  593. elseif arg[1] == '-l' or arg[1] == '--local-sync' then
  594. apply_xdelta = false
  595. table.remove(arg,1)
  596. elseif arg[1] == '-t' or arg[1] == '--translator' then
  597. set_translator(arg[2])
  598. table.remove(arg,1)
  599. table.remove(arg,1)
  600. elseif arg[1] == '--rename-only' then
  601. rename_only = true
  602. table.remove(arg,1)
  603. elseif arg[1] == '--override-db' then
  604. override_db = arg[2]
  605. table.remove(arg,1)
  606. table.remove(arg,1)
  607. else
  608. break
  609. end
  610. end
  611. if #arg < 2 then
  612. io.stderr:write(usage)
  613. os.exit(2)
  614. end
  615. -- here we go
  616. local endpoint = arg[1]
  617. table.remove(arg,1)
  618. local dbfile = nil
  619. if override_db ~= nil then
  620. dbfile = override_db:gsub('^~',os.getenv('HOME'))
  621. else
  622. dbfile = dbfile_name(endpoint, arg)
  623. end
  624. local xdelta = dbfile .. '.xdelta'
  625. local newdb = dbfile .. '.new'
  626. -- sanity check, translator and absolute paths cannot work
  627. for _, v in ipairs(arg) do
  628. if v:byte(1) == string.byte('/',1) then
  629. log_error("Absolute paths are not supported: "..v)
  630. log_tags_and_fail("Absolute path detected",
  631. "main","mailbox-has--absolute-path",true)
  632. end
  633. end
  634. -- we check the protocol version and dbfile fingerprint
  635. local firsttime = not exists(dbfile)
  636. if firsttime then
  637. log_progress('This is the first synchronization, '..
  638. 'verbose progress report enabled.')
  639. end
  640. log_progress('Phase 0: handshake')
  641. if firsttime then log_progress(' This phase opens the ssh connection.') end
  642. handshake(dbfile)
  643. -- receive and process commands
  644. local commands = receive_delta(io.stdin, firsttime)
  645. if rename_only then
  646. -- in renaming mode, we handle commands in a peculiar way
  647. log_progress('Phase 2: renaming script generation')
  648. local script = compute_renamings(commands)
  649. local fname = os.getenv('HOME')..'/smd-rename.sh'
  650. local f = io.open(fname,'w')
  651. f:write('#!/bin/sh\n\n')
  652. f:write(table.concat(script,'\n'))
  653. f:close()
  654. log('Please check and run: '..fname)
  655. -- and we exit
  656. os.exit(0)
  657. end
  658. log_progress('Phase 2: synchronization')
  659. if firsttime then
  660. log_progress([[
  661. This phase propagates the chages occurred to the remote mailbox to
  662. the local one.
  663. In the first run of smd-pull all remote emails are considered as
  664. new, and if not already present in the local mailbox, they are
  665. transferred over the ssh link.
  666. It is thus recommended to run the first synchronization on mailboxes
  667. that are resonably similar (i.e. not on an empty local mailbox).]])
  668. end
  669. for i,cmd in ipairs(commands) do
  670. local rc = execute(cmd)
  671. if not rc then
  672. io.write('ABORT\n')
  673. io.flush()
  674. os.exit(3)
  675. end
  676. -- some commands are delayed, we fire them in block
  677. if #get_full_email_queue > queue_max_len then
  678. process_pending_queue()
  679. end
  680. if firsttime and i % report_frequency == 0 then
  681. log_progress(string.format(' %3d%% complete', i / #commands * 100))
  682. end
  683. end
  684. -- some commands may still be in the queue, we fire them now
  685. process_pending_queue()
  686. -- we commit and update the dbfile
  687. log_progress('Phase 3: agreement')
  688. if firsttime then
  689. log_progress([[
  690. This last phase concludes the agreement between the remote and the
  691. local mailbox. In particular the s $tatus of the mailbox is sent
  692. from the remote host and stored locally.
  693. The status file size is circa 7M (3M compressed) for a 1G mailbox
  694. and it needs to be transferred completely only the first time.]])
  695. end
  696. io.write('COMMIT\n')
  697. io.flush()
  698. statistics.xdelta = receive(io.stdin, xdelta)
  699. local rc
  700. if not dry_run() and apply_xdelta then
  701. rc = os.execute(XDELTA..' patch '..xdelta..' '..dbfile..' '..newdb)
  702. else
  703. rc = 0 -- the xdelta transmitted with --dry-run is dummy
  704. end
  705. if rc ~= 0 and rc ~= 256 then
  706. log_error('Unable to apply delta to dbfile.')
  707. io.write('ABORT\n')
  708. io.flush()
  709. os.exit(4)
  710. end
  711. if not dry_run() and apply_xdelta then
  712. rc = os.rename(newdb,dbfile)
  713. else
  714. rc = true -- with --dry-run there is no xdelta affair
  715. end
  716. if not rc then
  717. log_error('Unable to rename '..newdb..' to '..dbfile)
  718. io.write('ABORT\n')
  719. io.flush()
  720. os.exit(5)
  721. end
  722. os.remove(xdelta)
  723. io.write('DONE\n')
  724. io.flush()
  725. -- some machine understandable output before quitting
  726. log_tag('stats::new-mails('.. statistics.added..
  727. '), del-mails('..statistics.removed..
  728. '), bytes-received('..statistics.received..
  729. '), xdelta-received('..statistics.xdelta..
  730. ')')
  731. if dry_run() and #statistics.files > 0 then
  732. log_tag('stats::mail-transferred('..
  733. table.concat(statistics.files,' , ')..')')
  734. end
  735. os.exit(0)
  736. end
  737. -- no more global variables
  738. set_strict()
  739. -- parachute for errors
  740. parachute(main, 6)
  741. -- vim:set ts=4: