├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── example.gbackup.toml ├── rust-toolchain.toml └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.swp 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | env: 7 | - RUST_BACKTRACE=1 8 | script: 9 | - cargo test --verbose 10 | - rustup component add rustfmt-preview 11 | - cargo fmt -- --check 12 | jobs: 13 | allow_failures: 14 | - rust: nightly 15 | fast_finish: true 16 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.13" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "arrayvec" 25 | version = "0.5.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" 28 | 29 | [[package]] 30 | name = "atty" 31 | version = "0.2.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 34 | dependencies = [ 35 | "hermit-abi", 36 | "libc", 37 | "winapi", 38 | ] 39 | 40 | [[package]] 41 | name = "autocfg" 42 | version = "1.0.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 45 | 46 | [[package]] 47 | name = "base64" 48 | version = "0.10.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" 51 | dependencies = [ 52 | "byteorder", 53 | ] 54 | 55 | [[package]] 56 | name = "base64" 57 | version = "0.12.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" 60 | 61 | [[package]] 62 | name = "base64" 63 | version = "0.13.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "1.2.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 72 | 73 | [[package]] 74 | name = "bufstream" 75 | version = "0.1.4" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" 78 | 79 | [[package]] 80 | name = "byteorder" 81 | version = "1.3.4" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 84 | 85 | [[package]] 86 | name = "cc" 87 | version = "1.0.59" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" 90 | 91 | [[package]] 92 | name = "cfg-if" 93 | version = "0.1.10" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 96 | 97 | [[package]] 98 | name = "charset" 99 | version = "0.1.2" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "4f426e64df1c3de26cbf44593c6ffff5dbfd43bbf9de0d075058558126b3fc73" 102 | dependencies = [ 103 | "base64 0.10.1", 104 | "encoding_rs", 105 | ] 106 | 107 | [[package]] 108 | name = "chrono" 109 | version = "0.4.19" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 112 | dependencies = [ 113 | "libc", 114 | "num-integer", 115 | "num-traits", 116 | "time", 117 | "winapi", 118 | ] 119 | 120 | [[package]] 121 | name = "clap" 122 | version = "2.33.3" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 125 | dependencies = [ 126 | "ansi_term", 127 | "atty", 128 | "bitflags", 129 | "strsim", 130 | "textwrap", 131 | "unicode-width", 132 | "vec_map", 133 | ] 134 | 135 | [[package]] 136 | name = "core-foundation" 137 | version = "0.7.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" 140 | dependencies = [ 141 | "core-foundation-sys", 142 | "libc", 143 | ] 144 | 145 | [[package]] 146 | name = "core-foundation-sys" 147 | version = "0.7.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" 150 | 151 | [[package]] 152 | name = "encoding_rs" 153 | version = "0.8.24" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "a51b8cf747471cb9499b6d59e59b0444f4c90eba8968c4e44874e92b5b64ace2" 156 | dependencies = [ 157 | "cfg-if", 158 | ] 159 | 160 | [[package]] 161 | name = "fallible-iterator" 162 | version = "0.2.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 165 | 166 | [[package]] 167 | name = "fallible-streaming-iterator" 168 | version = "0.1.9" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 171 | 172 | [[package]] 173 | name = "foreign-types" 174 | version = "0.3.2" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 177 | dependencies = [ 178 | "foreign-types-shared", 179 | ] 180 | 181 | [[package]] 182 | name = "foreign-types-shared" 183 | version = "0.1.1" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 186 | 187 | [[package]] 188 | name = "gbackup-rs" 189 | version = "0.2.0" 190 | dependencies = [ 191 | "chrono", 192 | "clap", 193 | "imap", 194 | "mailparse", 195 | "native-tls", 196 | "rusqlite", 197 | "serde", 198 | "thiserror", 199 | "threadpool", 200 | "toml", 201 | ] 202 | 203 | [[package]] 204 | name = "getrandom" 205 | version = "0.1.14" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 208 | dependencies = [ 209 | "cfg-if", 210 | "libc", 211 | "wasi 0.9.0+wasi-snapshot-preview1", 212 | ] 213 | 214 | [[package]] 215 | name = "hermit-abi" 216 | version = "0.1.15" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" 219 | dependencies = [ 220 | "libc", 221 | ] 222 | 223 | [[package]] 224 | name = "imap" 225 | version = "2.4.1" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "c617c55def8c42129e0dd503f11d7ee39d73f5c7e01eff55768b3879ff1d107d" 228 | dependencies = [ 229 | "base64 0.13.0", 230 | "bufstream", 231 | "chrono", 232 | "imap-proto", 233 | "lazy_static", 234 | "native-tls", 235 | "nom", 236 | "regex", 237 | ] 238 | 239 | [[package]] 240 | name = "imap-proto" 241 | version = "0.10.2" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f" 244 | dependencies = [ 245 | "nom", 246 | ] 247 | 248 | [[package]] 249 | name = "lazy_static" 250 | version = "1.4.0" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 253 | 254 | [[package]] 255 | name = "lexical-core" 256 | version = "0.7.4" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" 259 | dependencies = [ 260 | "arrayvec", 261 | "bitflags", 262 | "cfg-if", 263 | "ryu", 264 | "static_assertions", 265 | ] 266 | 267 | [[package]] 268 | name = "libc" 269 | version = "0.2.76" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" 272 | 273 | [[package]] 274 | name = "libsqlite3-sys" 275 | version = "0.20.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "e3a245984b1b06c291f46e27ebda9f369a94a1ab8461d0e845e23f9ced01f5db" 278 | dependencies = [ 279 | "pkg-config", 280 | "vcpkg", 281 | ] 282 | 283 | [[package]] 284 | name = "linked-hash-map" 285 | version = "0.5.3" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" 288 | 289 | [[package]] 290 | name = "log" 291 | version = "0.4.11" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 294 | dependencies = [ 295 | "cfg-if", 296 | ] 297 | 298 | [[package]] 299 | name = "lru-cache" 300 | version = "0.1.2" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" 303 | dependencies = [ 304 | "linked-hash-map", 305 | ] 306 | 307 | [[package]] 308 | name = "mailparse" 309 | version = "0.13.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "479b94621ea0fe875638d27f4a0b68213174b63e1ff9355d0948a04f71a5055a" 312 | dependencies = [ 313 | "base64 0.12.3", 314 | "charset", 315 | "quoted_printable", 316 | ] 317 | 318 | [[package]] 319 | name = "memchr" 320 | version = "2.3.3" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 323 | 324 | [[package]] 325 | name = "native-tls" 326 | version = "0.2.4" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" 329 | dependencies = [ 330 | "lazy_static", 331 | "libc", 332 | "log", 333 | "openssl", 334 | "openssl-probe", 335 | "openssl-sys", 336 | "schannel", 337 | "security-framework", 338 | "security-framework-sys", 339 | "tempfile", 340 | ] 341 | 342 | [[package]] 343 | name = "nom" 344 | version = "5.1.2" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" 347 | dependencies = [ 348 | "lexical-core", 349 | "memchr", 350 | "version_check", 351 | ] 352 | 353 | [[package]] 354 | name = "num-integer" 355 | version = "0.1.43" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" 358 | dependencies = [ 359 | "autocfg", 360 | "num-traits", 361 | ] 362 | 363 | [[package]] 364 | name = "num-traits" 365 | version = "0.2.12" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" 368 | dependencies = [ 369 | "autocfg", 370 | ] 371 | 372 | [[package]] 373 | name = "num_cpus" 374 | version = "1.13.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 377 | dependencies = [ 378 | "hermit-abi", 379 | "libc", 380 | ] 381 | 382 | [[package]] 383 | name = "openssl" 384 | version = "0.10.30" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" 387 | dependencies = [ 388 | "bitflags", 389 | "cfg-if", 390 | "foreign-types", 391 | "lazy_static", 392 | "libc", 393 | "openssl-sys", 394 | ] 395 | 396 | [[package]] 397 | name = "openssl-probe" 398 | version = "0.1.2" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 401 | 402 | [[package]] 403 | name = "openssl-sys" 404 | version = "0.9.58" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" 407 | dependencies = [ 408 | "autocfg", 409 | "cc", 410 | "libc", 411 | "pkg-config", 412 | "vcpkg", 413 | ] 414 | 415 | [[package]] 416 | name = "pkg-config" 417 | version = "0.3.18" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" 420 | 421 | [[package]] 422 | name = "ppv-lite86" 423 | version = "0.2.9" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" 426 | 427 | [[package]] 428 | name = "proc-macro2" 429 | version = "1.0.20" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "175c513d55719db99da20232b06cda8bab6b83ec2d04e3283edf0213c37c1a29" 432 | dependencies = [ 433 | "unicode-xid", 434 | ] 435 | 436 | [[package]] 437 | name = "quote" 438 | version = "1.0.7" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 441 | dependencies = [ 442 | "proc-macro2", 443 | ] 444 | 445 | [[package]] 446 | name = "quoted_printable" 447 | version = "0.4.2" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2" 450 | 451 | [[package]] 452 | name = "rand" 453 | version = "0.7.3" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 456 | dependencies = [ 457 | "getrandom", 458 | "libc", 459 | "rand_chacha", 460 | "rand_core", 461 | "rand_hc", 462 | ] 463 | 464 | [[package]] 465 | name = "rand_chacha" 466 | version = "0.2.2" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 469 | dependencies = [ 470 | "ppv-lite86", 471 | "rand_core", 472 | ] 473 | 474 | [[package]] 475 | name = "rand_core" 476 | version = "0.5.1" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 479 | dependencies = [ 480 | "getrandom", 481 | ] 482 | 483 | [[package]] 484 | name = "rand_hc" 485 | version = "0.2.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 488 | dependencies = [ 489 | "rand_core", 490 | ] 491 | 492 | [[package]] 493 | name = "redox_syscall" 494 | version = "0.1.57" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 497 | 498 | [[package]] 499 | name = "regex" 500 | version = "1.3.9" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 503 | dependencies = [ 504 | "aho-corasick", 505 | "memchr", 506 | "regex-syntax", 507 | "thread_local", 508 | ] 509 | 510 | [[package]] 511 | name = "regex-syntax" 512 | version = "0.6.18" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 515 | 516 | [[package]] 517 | name = "remove_dir_all" 518 | version = "0.5.3" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 521 | dependencies = [ 522 | "winapi", 523 | ] 524 | 525 | [[package]] 526 | name = "rusqlite" 527 | version = "0.24.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "4c78c3275d9d6eb684d2db4b2388546b32fdae0586c20a82f3905d21ea78b9ef" 530 | dependencies = [ 531 | "bitflags", 532 | "fallible-iterator", 533 | "fallible-streaming-iterator", 534 | "libsqlite3-sys", 535 | "lru-cache", 536 | "memchr", 537 | "smallvec", 538 | ] 539 | 540 | [[package]] 541 | name = "ryu" 542 | version = "1.0.5" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 545 | 546 | [[package]] 547 | name = "schannel" 548 | version = "0.1.19" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" 551 | dependencies = [ 552 | "lazy_static", 553 | "winapi", 554 | ] 555 | 556 | [[package]] 557 | name = "security-framework" 558 | version = "0.4.4" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" 561 | dependencies = [ 562 | "bitflags", 563 | "core-foundation", 564 | "core-foundation-sys", 565 | "libc", 566 | "security-framework-sys", 567 | ] 568 | 569 | [[package]] 570 | name = "security-framework-sys" 571 | version = "0.4.3" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" 574 | dependencies = [ 575 | "core-foundation-sys", 576 | "libc", 577 | ] 578 | 579 | [[package]] 580 | name = "serde" 581 | version = "1.0.115" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" 584 | dependencies = [ 585 | "serde_derive", 586 | ] 587 | 588 | [[package]] 589 | name = "serde_derive" 590 | version = "1.0.115" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" 593 | dependencies = [ 594 | "proc-macro2", 595 | "quote", 596 | "syn", 597 | ] 598 | 599 | [[package]] 600 | name = "smallvec" 601 | version = "1.4.2" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" 604 | 605 | [[package]] 606 | name = "static_assertions" 607 | version = "1.1.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 610 | 611 | [[package]] 612 | name = "strsim" 613 | version = "0.8.0" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 616 | 617 | [[package]] 618 | name = "syn" 619 | version = "1.0.40" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "963f7d3cc59b59b9325165add223142bbf1df27655d07789f109896d353d8350" 622 | dependencies = [ 623 | "proc-macro2", 624 | "quote", 625 | "unicode-xid", 626 | ] 627 | 628 | [[package]] 629 | name = "tempfile" 630 | version = "3.1.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 633 | dependencies = [ 634 | "cfg-if", 635 | "libc", 636 | "rand", 637 | "redox_syscall", 638 | "remove_dir_all", 639 | "winapi", 640 | ] 641 | 642 | [[package]] 643 | name = "textwrap" 644 | version = "0.11.0" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 647 | dependencies = [ 648 | "unicode-width", 649 | ] 650 | 651 | [[package]] 652 | name = "thiserror" 653 | version = "1.0.20" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" 656 | dependencies = [ 657 | "thiserror-impl", 658 | ] 659 | 660 | [[package]] 661 | name = "thiserror-impl" 662 | version = "1.0.20" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" 665 | dependencies = [ 666 | "proc-macro2", 667 | "quote", 668 | "syn", 669 | ] 670 | 671 | [[package]] 672 | name = "thread_local" 673 | version = "1.0.1" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 676 | dependencies = [ 677 | "lazy_static", 678 | ] 679 | 680 | [[package]] 681 | name = "threadpool" 682 | version = "1.8.1" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" 685 | dependencies = [ 686 | "num_cpus", 687 | ] 688 | 689 | [[package]] 690 | name = "time" 691 | version = "0.1.44" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 694 | dependencies = [ 695 | "libc", 696 | "wasi 0.10.0+wasi-snapshot-preview1", 697 | "winapi", 698 | ] 699 | 700 | [[package]] 701 | name = "toml" 702 | version = "0.5.6" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" 705 | dependencies = [ 706 | "serde", 707 | ] 708 | 709 | [[package]] 710 | name = "unicode-width" 711 | version = "0.1.8" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 714 | 715 | [[package]] 716 | name = "unicode-xid" 717 | version = "0.2.1" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 720 | 721 | [[package]] 722 | name = "vcpkg" 723 | version = "0.2.10" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" 726 | 727 | [[package]] 728 | name = "vec_map" 729 | version = "0.8.2" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 732 | 733 | [[package]] 734 | name = "version_check" 735 | version = "0.9.2" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 738 | 739 | [[package]] 740 | name = "wasi" 741 | version = "0.9.0+wasi-snapshot-preview1" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 744 | 745 | [[package]] 746 | name = "wasi" 747 | version = "0.10.0+wasi-snapshot-preview1" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 750 | 751 | [[package]] 752 | name = "winapi" 753 | version = "0.3.9" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 756 | dependencies = [ 757 | "winapi-i686-pc-windows-gnu", 758 | "winapi-x86_64-pc-windows-gnu", 759 | ] 760 | 761 | [[package]] 762 | name = "winapi-i686-pc-windows-gnu" 763 | version = "0.4.0" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 766 | 767 | [[package]] 768 | name = "winapi-x86_64-pc-windows-gnu" 769 | version = "0.4.0" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 772 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gbackup-rs" 3 | version = "0.2.0" 4 | authors = ["Nikola Dipanov "] 5 | edition = "2018" 6 | description = "Fast and configurable CLI tool to back-up your GMail data" 7 | license = "MIT" 8 | keywords = ["gmail", "backup", "cli"] 9 | categories = ["command-line-utilities"] 10 | repository = "https://github.com/djipko/gbackup-rs/" 11 | 12 | [dependencies] 13 | chrono = "0.4.19" 14 | clap = "2.27.1" 15 | imap = "2.4.1" 16 | mailparse = "0.13.0" 17 | native-tls = "0.2.4" 18 | rusqlite = "0.24.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | thiserror = "1.0" 21 | threadpool = "1.8.1" 22 | toml = "0.5.6" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Nikola Dipanov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gbackup-rs - Backup your GMail 2 | 3 | [![Build Status](https://travis-ci.org/djipko/gbackup-rs.svg?branch=master)](https://travis-ci.org/djipko/gbackup-rs) 4 | [![Crates.io](https://img.shields.io/crates/v/gbackup-rs.svg)](https://crates.io/crates/gbackup-rs) 5 | 6 | gbackup-rs is yet another tool for backing up your GMail account with a few 7 | features worth calling out: 8 | 9 | * It does incremental backups 10 | * Uses an easy to understand config format and can handle multiple GMail accounts 11 | * Backup is transactional, meaning less likelihood of corrupt backups 12 | * It will pull email down in parallel which can result in a 5x + speed-up of the initial backup 13 | * Written in Rust meaning more likely to be fast and secure 14 | 15 | It uses a custom schema (a sqlite db) by default to store email - and tools to 16 | export to commonly used formats (such as, say, mbox) are separate (adhering to 17 | good ol' unix philosophy of doing only one thing) 18 | 19 | ## Design 20 | 21 | gbackup-rs uses IMAP to sync email but does not aim to replicate all of your Gmail 22 | mailbox layout. Its main goal is to back data up, and not be a full-fledged IMAP e-mail client. 23 | This may change in the future if it turns out to be useful, but for now the 24 | main goal of the design is to allow for fast backups and prevent data-loss, should 25 | you lose access to your Gmail account for whatever reason (after ~15 years of using GMail - I personally 26 | have invaluable data from several stages of life that would be really hard to replace). 27 | 28 | ## Installing 29 | 30 | The easiest way to install this software is using `cargo`. If you have cargo installed, 31 | simply run: 32 | 33 | ```bash 34 | cargo install gbackup-rs 35 | ``` 36 | 37 | Installing cargo itself can be done by following the guide to installing the Rust 38 | environment using `rustup` as described [here](https://www.rust-lang.org/tools/install). 39 | 40 | ## Usage 41 | 42 | gbackup-rs is driven by a single configuration file. This is best explained with 43 | a simple example - there is an example of a config file in the top level directory. 44 | A simple and rather self-explanatory config would look something like: 45 | 46 | ```toml 47 | [[accounts]] 48 | username = "firstname.lastname@gmail.com" 49 | [accounts.password] 50 | type = "EnvVar" 51 | name = "GMAIL_PASSWORD" 52 | [accounts.backup] 53 | type = "Sqlite" 54 | backup_dir = "/Users/firstname/gmail_backup_test/" 55 | [accounts.export] 56 | type = "Mbox" 57 | path = "/Users/firstname/gmail.mbox" 58 | ``` 59 | 60 | and by default it's expected to be stored in a file called `.gbackup.toml` in the 61 | working directory (but is configurable via the -c option). Then you would simply run: 62 | 63 | ```bash 64 | GMAIL_PASSWORD="mysecretpass" gbackup-rs -w 10 65 | ``` 66 | 67 | This is recommended for the first back-up as it will pull down email using 68 | 10 parallel IMAP connections. This results in a significant speed up! On a ~4Gb 69 | mailbox - a single connection backup took ~19 minutes and 5 minutes 70 | with 10 parallel connections. Since subsequent backups are incremental - running with 71 | a single worker (the default) should be fine for low volume personal accounts. 72 | 73 | The resulting backup can then be found in the configured directory as `backup.sqlite` 74 | 75 | NOTE that you will need to set up an app password for you gmail account 76 | [here](https://myaccount.google.com/apppasswords) 77 | 78 | Exporting the data to a widely used format such as [mbox](http://qmail.org./man/man5/mbox.html) 79 | can be done by running with the `export` subcommand 80 | 81 | ```bash 82 | gbackup-rs -c ~/.gbackup.toml export 83 | ``` 84 | 85 | This will in turn run any export engines configured for each account in the config, 86 | if any. 87 | 88 | ## Future features 89 | 90 | gbackup-rs is highly experimental at this point and many things are likely to 91 | change (hopefully for the better). 92 | Check out the issues tracker to see some of the stuff that's been worked on. 93 | -------------------------------------------------------------------------------- /example.gbackup.toml: -------------------------------------------------------------------------------- 1 | [[accounts]] 2 | username = "firstname.lastname@gmail.com" 3 | [accounts.password] 4 | type = "EnvVar" 5 | name = "MAIN_GMAIL_PASS" 6 | [accounts.backup] 7 | type = "Sqlite" 8 | backup_dir = "/Users/firstname/gmail_backup_main/" 9 | [accounts.export] 10 | type = "Mbox" 11 | path = "/Users/firstname/gmail.mbox" 12 | 13 | [[accounts]] 14 | username = "alter.ego@gmail.com" 15 | [accounts.password] 16 | type = "EnvVar" 17 | name = "OTHER_GMAIL_PASS" 18 | [accounts.backup] 19 | type = "Sqlite" 20 | backup_dir = "/Users/firstname/gmail_backup_alterego/" 21 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.52.0" 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! # gbackup-rs - Backup your GMail 2 | //! 3 | //! gbackup-rs is yet another tool for backing up your GMail account with a few 4 | //! features worth calling out: 5 | //! 6 | //! * It does incremental backups 7 | //! * Uses an easy to understand config format and can handle multiple GMail accounts 8 | //! * Backup is transactional, meaning less likelihood of corrupt backups 9 | //! * It will pull email down in parallel which can result in a 5x + speed-up of the initial backup 10 | //! * Written in Rust meaning more likely to be fast and secure 11 | //! 12 | //! ## Design 13 | //! 14 | //! gbackup-rs uses IMAP to sync email but does not aim to replicate all of your Gmail 15 | //! mailbox layout. Its main goal is to back-up data, and not be a full-fledged IMAP e-mail client. 16 | //! This may change in the future if it turns out to be useful, but for now the 17 | //! main goal of the design is to allow for fast backups and prevent data-loss, should 18 | //! you lose access to your Gmail account for whatever reason (after ~15 years of using GMail - I personally 19 | //! have invaluable data from several stages of life that would be really hard to replace). 20 | //! 21 | //! It uses a custom schema (a sqlite db) by default to store email - and tools to 22 | //! export to commonly used formats (such as, say, mbox) are separate (adhering to 23 | //! good ol' unix philosophy of doing only one thing) 24 | //! 25 | //! ## Usage 26 | //! 27 | //! gbackup-rs is driven by a single configuration file. This is best explained with 28 | //! a simple example - there is an example of a config file in the top level directory. 29 | //! A simple and rather self-explanatory config would look something like: 30 | //! 31 | //! ```toml 32 | //! [[accounts]] 33 | //! username = "firstname.lastname@gmail.com" 34 | //! [accounts.password] 35 | //! type = "EnvVar" 36 | //! name = "GMAIL_PASSWORD" 37 | //! [accounts.backup] 38 | //! type = "Sqlite" 39 | //! backup_dir = "/Users/firstname/gmail_backup_test/" 40 | //! [accounts.export] 41 | //! type = "Mbox" 42 | //! path = "/Users/firstname/gmail.mbox" 43 | //! ``` 44 | //! 45 | //! and by default it's expected to be stored in a file called `.gbackup.toml` in the 46 | //! working directory (but is configurable via the -c option). Then you would simply run: 47 | //! 48 | //! ```bash 49 | //! GMAIL_PASSWORD="mysecretpass" gbackup-rs -w 10 50 | //! ``` 51 | //! 52 | //! This is recommended for the first back-up as it will pull down email using 53 | //! 10 parallel IMAP connections. This results in a significant speed up! On a ~4Gb 54 | //! mailbox - a single connection backup took ~19 minutes and 5 minutes 55 | //! with 10 parallel connections. Since subsequent backups are incremental - running with 56 | //! a single worker (the default) should be fine for low volume personal accounts. 57 | //! 58 | //! The resulting backup can then be found in the configured directory as `backup.sqlite` 59 | //! 60 | //! Exporting the data to a widely used format such as [mbox](http://qmail.org./man/man5/mbox.html) 61 | //! can be done by running with the `export` subcommand 62 | //! 63 | //! ```bash 64 | //! gbackup-rs -c ~/.gbackup.toml export 65 | //! ``` 66 | //! 67 | //! This will in turn run any export engines configured for each account in the config, 68 | //! if any. 69 | 70 | use chrono::{TimeZone, Utc}; 71 | use clap::{App, Arg, SubCommand}; 72 | use imap::error::Error as IMAPError; 73 | use imap::types::{Fetch, Uid, ZeroCopy}; 74 | use imap::Session; 75 | use mailparse; 76 | use mailparse::MailHeaderMap; 77 | use native_tls; 78 | use native_tls::Error as TLSError; 79 | use rusqlite; 80 | use serde::Deserialize; 81 | use std::collections::HashSet; 82 | use std::error::Error as StdError; 83 | use std::fs; 84 | use std::fs::File; 85 | use std::io; 86 | use std::io::Write; 87 | use std::iter::FromIterator; 88 | use std::path::Path; 89 | use std::str; 90 | use std::sync::mpsc::channel; 91 | use std::sync::{Arc, RwLock}; 92 | use thiserror::Error; 93 | use threadpool::ThreadPool; 94 | use toml; 95 | 96 | type TLSStream = native_tls::TlsStream; 97 | type Rfc822Data = Vec; 98 | 99 | const ALL_MAIL_INBOX: &str = "[Gmail]/All Mail"; 100 | 101 | #[derive(Debug, Deserialize)] 102 | #[serde(tag = "type")] 103 | enum LoadPassword { 104 | EnvVar { name: String }, 105 | } 106 | 107 | #[derive(Debug, Deserialize)] 108 | #[serde(tag = "type")] 109 | enum BackupEngineConfig { 110 | Stdout { count: usize }, 111 | Sqlite { backup_dir: String }, 112 | } 113 | 114 | #[derive(Debug, Deserialize)] 115 | #[serde(tag = "type")] 116 | enum ExportEngineConfig { 117 | Mbox { path: Option }, 118 | } 119 | 120 | #[derive(Debug, Deserialize)] 121 | struct GBackupAccount { 122 | username: String, 123 | password: LoadPassword, 124 | backup: Option, 125 | export: Option, 126 | } 127 | 128 | #[derive(Debug, Deserialize)] 129 | struct GBackupConfig { 130 | accounts: Vec, 131 | } 132 | 133 | #[derive(Debug, Error)] 134 | enum BackupEngineError { 135 | #[error("Backup engine failed to load: {0}!")] 136 | EngineLoadError(String), 137 | #[error("Backup engine issue with Sqlite DB: {0}!")] 138 | EngineRunSqliteError(#[from] rusqlite::Error), 139 | } 140 | 141 | #[derive(Debug, Error)] 142 | enum ExportEngineError { 143 | #[error("Export engine issue with Sqlite DB: {0}!")] 144 | SqliteError(#[from] rusqlite::Error), 145 | #[error("Export engine issue while loading data from Sqlite DB: {0}!")] 146 | SqliteReadError(#[from] rusqlite::types::FromSqlError), 147 | #[error("Operation not supported by engine")] 148 | UnsupportedOp, 149 | #[error("Export engine IO error: {0}")] 150 | IOErrror(#[from] io::Error), 151 | #[error("Error converting to Mbox fromat: {0}")] 152 | MboxFormatError(String), 153 | #[error("Export engine error parsing email: {0}")] 154 | MailParseError(#[from] mailparse::MailParseError), 155 | } 156 | 157 | #[derive(Debug, Error)] 158 | enum GBackupError { 159 | #[error("TLS error {0}")] 160 | TLSError(#[from] TLSError), 161 | #[error("IMAP error {0}")] 162 | IMAPError(#[from] IMAPError), 163 | #[error("Error loading environment var {0}")] 164 | VarError(#[from] std::env::VarError), 165 | #[error("Failed to load backup config for: {0}!")] 166 | BackupConfigError(String), 167 | #[error("Error returned by the backup engine: {0}!")] 168 | BackupError(#[from] BackupEngineError), 169 | #[error("Failed to load export config for: {0}!")] 170 | ExportConfigError(String), 171 | #[error("Error returned by the export engine: {0}!")] 172 | ExportError(#[from] ExportEngineError), 173 | #[error("Error parsing rfc822 data: {0}!")] 174 | MailParseError(#[from] mailparse::MailParseError), 175 | } 176 | 177 | struct ExportEmail { 178 | raw_data: Rfc822Data, 179 | uid: Uid, 180 | } 181 | 182 | /// Main trait used to implement a backup strategy - see method docs for more 183 | /// details on how this can be used, and see the 2 implemented engines 184 | /// `StdoutEngine` and `SqliteEngine` for details 185 | trait BackupEngine { 186 | /// This method should expect to be passed a list of all available UIDs 187 | /// (potentially for a specific mailbox, in a future implementation) and it 188 | /// should return a set of those that need to be backed up, potentially 189 | /// reporting an error. 190 | /// 191 | /// This will be used to implement incremental backups so that we don't pull 192 | /// down unnecessary emails 193 | fn filter_for_backup( 194 | &self, 195 | search_uids: &HashSet, 196 | ) -> Result, BackupEngineError>; 197 | /// This method should implement the main back up logic, and will be passed 198 | /// emails downloaded. For more details on the Fetch struct see it's docs 199 | /// in the imap crate. Any progress reporting should be done from this 200 | /// method too - it will be called only once per each GMail account in the config 201 | fn run_backup(&mut self, to_backup: &Vec<&Fetch>) -> Result<(), BackupEngineError>; 202 | 203 | /// If an engine can be used for exporting data - this is the method to 204 | /// implement that will be called and pass the data to a struct implementing 205 | /// the `ExportEngine` trait (see bellow). `SqliteEngine` provides a good 206 | /// example of this 207 | fn get_all_emails_raw(&self) -> Result, ExportEngineError> { 208 | Err(ExportEngineError::UnsupportedOp) 209 | } 210 | } 211 | 212 | struct StdoutEngine<'a> { 213 | #[allow(dead_code)] 214 | account: &'a GBackupAccount, 215 | count: usize, 216 | } 217 | 218 | impl<'a> StdoutEngine<'a> { 219 | fn new(account: &'a GBackupAccount) -> Result, BackupEngineError> { 220 | if let GBackupAccount { 221 | backup: Some(BackupEngineConfig::Stdout { count }), 222 | .. 223 | } = account 224 | { 225 | Ok(Box::new(StdoutEngine { 226 | account, 227 | count: *count, 228 | })) 229 | } else { 230 | Err(BackupEngineError::EngineLoadError(format!( 231 | "Can't init Stdout engine from account config {:?}", 232 | account 233 | ))) 234 | } 235 | } 236 | } 237 | 238 | impl<'a> BackupEngine for StdoutEngine<'a> { 239 | fn filter_for_backup( 240 | &self, 241 | search_uids: &HashSet, 242 | ) -> Result, BackupEngineError> { 243 | Ok(HashSet::from_iter( 244 | search_uids.iter().copied().take(self.count), 245 | )) 246 | } 247 | 248 | fn run_backup(&mut self, to_backup: &Vec<&Fetch>) -> Result<(), BackupEngineError> { 249 | to_backup 250 | .iter() 251 | .map(|f| { 252 | println!( 253 | "Would back up message w subject: {}", 254 | String::from_utf8(f.envelope().unwrap().subject.unwrap().to_vec()).unwrap() 255 | ) 256 | }) 257 | .for_each(drop); 258 | Ok(()) 259 | } 260 | } 261 | 262 | struct SqliteEngine<'a> { 263 | #[allow(dead_code)] 264 | account: &'a GBackupAccount, 265 | conn: rusqlite::Connection, 266 | } 267 | 268 | impl<'a> SqliteEngine<'a> { 269 | fn new(account: &'a GBackupAccount) -> Result, BackupEngineError> { 270 | if let GBackupAccount { 271 | backup: Some(BackupEngineConfig::Sqlite { backup_dir }), 272 | .. 273 | } = account 274 | { 275 | let db = Path::new(backup_dir).join("backup.sqlite"); 276 | let conn = rusqlite::Connection::open(db)?; 277 | conn.execute( 278 | "CREATE TABLE IF NOT EXISTS email ( 279 | uid INTEGER PRIMARY KEY, 280 | rfc822_body BLOB 281 | )", 282 | rusqlite::params![], 283 | )?; 284 | Ok(Box::new(SqliteEngine { account, conn })) 285 | } else { 286 | Err(BackupEngineError::EngineLoadError(format!( 287 | "Can't init Sqlite engine from account config {:?}", 288 | account 289 | ))) 290 | } 291 | } 292 | } 293 | 294 | impl<'s> BackupEngine for SqliteEngine<'s> { 295 | fn filter_for_backup( 296 | &self, 297 | search_uids: &HashSet, 298 | ) -> Result, BackupEngineError> { 299 | let mut stmt = self.conn.prepare("SELECT uid FROM email")?; 300 | let backed_up_uids = stmt 301 | .query_map(rusqlite::NO_PARAMS, |row| row.get::<_, Uid>(0))? 302 | .collect::, _>>()?; 303 | let to_backup = search_uids.difference(&backed_up_uids).copied().collect(); 304 | Ok(to_backup) 305 | } 306 | 307 | fn run_backup(&mut self, to_backup: &Vec<&Fetch>) -> Result<(), BackupEngineError> { 308 | let tx = self.conn.transaction()?; 309 | let mut stmt = tx.prepare("INSERT OR REPLACE INTO email VALUES (?, ?)")?; 310 | println!("To backup: {:?}", to_backup.len()); 311 | let backed_up_ids = to_backup 312 | .iter() 313 | .map(|f| { 314 | if let Fetch { uid: Some(uid), .. } = f { 315 | stmt.execute(rusqlite::params![&uid, f.body(),]) 316 | } else { 317 | Ok(0) 318 | } 319 | }) 320 | .collect::, _>>()?; 321 | drop(stmt); 322 | tx.commit()?; 323 | println!( 324 | "Backed up {}/{} emails successfully", 325 | backed_up_ids 326 | .iter() 327 | .filter_map(|id| if *id != 0 { Some(id) } else { None }) 328 | .count(), 329 | to_backup.len() 330 | ); 331 | Ok(()) 332 | } 333 | 334 | fn get_all_emails_raw(&self) -> Result, ExportEngineError> { 335 | let mut stmt = self.conn.prepare("SELECT uid, rfc822_body FROM email")?; 336 | let emails = stmt 337 | .query_map(rusqlite::NO_PARAMS, |row| { 338 | let uid = row.get(0)?; 339 | let raw_data = row.get(1)?; 340 | Ok(ExportEmail { raw_data, uid }) 341 | })? 342 | .collect::, _>>()?; 343 | Ok(emails) 344 | } 345 | } 346 | 347 | fn get_backup_engine<'a>( 348 | account: &'a GBackupAccount, 349 | ) -> Result, GBackupError> { 350 | match account.backup { 351 | Some(BackupEngineConfig::Stdout { .. }) => Ok(StdoutEngine::new(account)?), 352 | Some(BackupEngineConfig::Sqlite { .. }) => Ok(SqliteEngine::new(account)?), 353 | _ => Err(GBackupError::BackupConfigError(format!( 354 | "Can't load engine from the empty backup config for {:?}", 355 | account 356 | ))), 357 | } 358 | } 359 | 360 | struct BackupRunner { 361 | account: Arc>, 362 | pool: ThreadPool, 363 | } 364 | 365 | impl BackupRunner { 366 | fn new(account: GBackupAccount, workers: usize) -> BackupRunner { 367 | BackupRunner { 368 | account: Arc::new(RwLock::new(account)), 369 | pool: ThreadPool::new(workers), 370 | } 371 | } 372 | 373 | fn new_imap_session( 374 | account: &Arc>, 375 | ) -> Result, GBackupError> { 376 | // TODO: Implement loading of paswords and othe auth options probably 377 | // to be pulled out into a single method 378 | let ro_acct = account.read().unwrap(); 379 | let LoadPassword::EnvVar { name } = &ro_acct.password; 380 | let password = std::env::var(&name)?; 381 | 382 | let tls = native_tls::TlsConnector::new()?; 383 | let client = imap::connect(("imap.gmail.com", 993), "imap.gmail.com", &tls)?; 384 | 385 | let session = client.login(&ro_acct.username, password).map_err(|e| e.0)?; 386 | println!("Successfully created IMAP session for {}", ro_acct.username); 387 | 388 | Ok(session) 389 | } 390 | 391 | fn do_fetch( 392 | account: Arc>, 393 | chunk: &[Uid], 394 | ) -> Result>, GBackupError> { 395 | let mut session = BackupRunner::new_imap_session(&account)?; 396 | session.select(ALL_MAIL_INBOX)?; 397 | let fetched = session.uid_fetch( 398 | Vec::from_iter(chunk.iter().map(|uid| uid.to_string())).join(","), 399 | "BODY.PEEK[]", 400 | )?; 401 | Ok(fetched) 402 | } 403 | 404 | fn run_backup(self) -> Result<(), GBackupError> { 405 | let ro_acct = self.account.read().unwrap(); 406 | let mut engine = get_backup_engine(&ro_acct)?; 407 | let mut session = BackupRunner::new_imap_session(&self.account)?; 408 | // TODO: This should also be configurable, we may want to backup all mail, 409 | // specific labes, etc... note also that the mailbox names may not be the same 410 | // depending on the language 411 | session.select(ALL_MAIL_INBOX)?; 412 | let uids = session.uid_search("ALL")?; 413 | println!( 414 | "Found {} messages in total for account {}", 415 | uids.len(), 416 | ro_acct.username 417 | ); 418 | 419 | let uids_to_backup = engine.filter_for_backup(&uids)?; 420 | println!( 421 | "Uids that need backing up for account {}: {:?}", 422 | ro_acct.username, uids_to_backup 423 | ); 424 | let chunk_len = (uids_to_backup.len() / self.pool.max_count()) + 1; 425 | if !uids_to_backup.is_empty() { 426 | println!( 427 | "Downloading messages for {} with {} workers", 428 | ro_acct.username, 429 | self.pool.max_count() 430 | ); 431 | let (tx, rx) = channel(); 432 | Vec::from_iter(uids_to_backup) 433 | .chunks(chunk_len) 434 | .map(|chunk| { 435 | // TODO: Instead of this copy over here - we could use 436 | // scoped thread from crossbeam crate 437 | // (not supported by the thread pool though) 438 | let curr_chunk_len = chunk.len(); 439 | let mut chunk_vec = Vec::with_capacity(curr_chunk_len); 440 | chunk_vec.resize(curr_chunk_len, 0); 441 | chunk_vec.copy_from_slice(chunk); 442 | let tx = tx.clone(); 443 | let account = Arc::clone(&self.account); 444 | self.pool.execute(move || { 445 | let fetch_res = BackupRunner::do_fetch(account, &chunk_vec); 446 | // This can panic but it's unlikely! 447 | tx.send(fetch_res).unwrap(); 448 | }) 449 | }) 450 | .for_each(drop); 451 | self.pool.join(); 452 | let emails = rx 453 | .try_iter() 454 | .collect::>>, _>>()?; 455 | // Server can include so called unilateral responses - drop them if they don't 456 | // have a body 457 | let emails = emails 458 | .iter() 459 | .flatten() 460 | .filter(|&f| f.uid.is_some() & f.body().is_some()) 461 | .collect(); 462 | println!("Backing up messages for {}", ro_acct.username); 463 | engine.run_backup(&emails)?; 464 | } 465 | Ok(()) 466 | } 467 | } 468 | 469 | /// Trait that is used to implement an export strategy. This is not necessarily 470 | /// the same as a backup strategy as backing up and exporting email for further 471 | /// reading/storing/processing can have different requirements. This is 472 | /// illustrated well by the `SqliteEngine` which is a backup engine and allows 473 | /// us to do fast, incremental, failure-proof backups using seasoned technology 474 | /// like an sqlite DB, which we could not do with a flat file like say mbox 475 | /// format which is very popular with email clients. We still want to be able to 476 | /// export our email into a more widely used format, and this separation of 477 | /// concerns is the reason for separate traits. 478 | trait ExportEngine { 479 | /// Implemented this to do any of the exporting logic (including parsing) 480 | /// of emails, which is left to each engine, so as to be able to avoid doing 481 | /// unnecessary work if parsing of some parts is not needed. This method 482 | /// should do any I/O work, and return an appropriate error when needed. 483 | fn export_emails(&self, emails: &Vec) -> Result<(), ExportEngineError>; 484 | } 485 | 486 | struct MboxEngine<'a> { 487 | account: &'a GBackupAccount, 488 | } 489 | 490 | impl<'a> MboxEngine<'a> { 491 | fn get_from_line(email: &ExportEmail) -> Result, ExportEngineError> { 492 | let (headers, _) = mailparse::parse_headers(&email.raw_data).or(Err( 493 | ExportEngineError::MboxFormatError(format!( 494 | "Can't parse headers for email: {}!", 495 | email.uid 496 | )), 497 | ))?; 498 | let from_h = headers 499 | .iter() 500 | .filter(|h| h.get_key().to_lowercase() == "from") 501 | .next() 502 | .ok_or(ExportEngineError::MboxFormatError(format!( 503 | "Can't find 'From' header in: {:?}", 504 | email.uid 505 | )))?; 506 | let date_str = 507 | headers 508 | .get_first_value("Date") 509 | .ok_or(ExportEngineError::MboxFormatError(format!( 510 | "Can't find 'Date' header in: {:?}", 511 | email.uid 512 | )))?; 513 | 514 | let addr = mailparse::addrparse_header(&from_h) 515 | .or(Err(ExportEngineError::MboxFormatError(format!( 516 | "Can't parse address from header in email: {}!", 517 | email.uid 518 | ))))? 519 | .extract_single_info() 520 | .ok_or(ExportEngineError::MboxFormatError(format!( 521 | "Can't find a single address from header in email: {}", 522 | email.uid 523 | )))? 524 | .addr; 525 | let date = mailparse::dateparse(&date_str).or(Err(ExportEngineError::MboxFormatError( 526 | format!("Can't parse Date from header in email: {}", email.uid), 527 | )))?; 528 | let mut buffer = Vec::new(); 529 | write!( 530 | &mut buffer, 531 | "From {} {}\n", 532 | addr, 533 | Utc.timestamp(date, 0).format("%a %b %d %T %Y") 534 | )?; 535 | Ok(buffer) 536 | } 537 | } 538 | 539 | impl ExportEngine for MboxEngine<'_> { 540 | fn export_emails(&self, emails: &Vec) -> Result<(), ExportEngineError> { 541 | let mut f: Box; 542 | if let Some(ExportEngineConfig::Mbox { path: Some(path) }) = &self.account.export { 543 | f = Box::new(File::create(path)?); 544 | } else { 545 | f = Box::new(io::stdout()); 546 | } 547 | // TODO: Make this parallel! 548 | emails 549 | .iter() 550 | .map(|raw_email| { 551 | match MboxEngine::get_from_line(raw_email) { 552 | Ok(from_line) => { 553 | f.write_all(&from_line)?; 554 | f.write_all(&raw_email.raw_data)?; 555 | f.write_all(b"\n")?; 556 | } 557 | Err(e) => { 558 | println!("Error parsing email: {:?}!", e); 559 | } 560 | } 561 | Ok(()) 562 | }) 563 | .collect::>()?; 564 | 565 | Ok(()) 566 | } 567 | } 568 | 569 | fn get_export_engine<'a>( 570 | account: &'a GBackupAccount, 571 | ) -> Result, GBackupError> { 572 | match account.export { 573 | Some(ExportEngineConfig::Mbox { .. }) => Ok(Box::new(MboxEngine { account })), 574 | _ => Err(GBackupError::ExportConfigError(format!( 575 | "Can't load engine from the empty export config for {:?}", 576 | account 577 | ))), 578 | } 579 | } 580 | 581 | struct ExportRunner { 582 | account: GBackupAccount, 583 | } 584 | 585 | impl ExportRunner { 586 | fn run_export(self) -> Result<(), GBackupError> { 587 | let backup_engine = get_backup_engine(&self.account)?; 588 | let export_engine = get_export_engine(&self.account)?; 589 | 590 | let emails = backup_engine.get_all_emails_raw()?; 591 | export_engine.export_emails(&emails)?; 592 | Ok(()) 593 | } 594 | } 595 | 596 | fn main() -> Result<(), Box> { 597 | let matches = App::new("Gmail backup") 598 | .version("0.1") 599 | .author("Nikola Dipanov") 600 | .about("Backup your gmail stuff") 601 | .arg( 602 | Arg::with_name("config") 603 | .short("c") 604 | .long("config") 605 | .value_name("FILE") 606 | .help("Sets a custom config file") 607 | .takes_value(true) 608 | .default_value(".gbackup.toml"), 609 | ) 610 | .arg( 611 | Arg::with_name("workers") 612 | .short("w") 613 | .long("workers") 614 | .value_name("N_WORKERS") 615 | .help("Number of concurrent IMAP connections") 616 | .takes_value(true) 617 | .default_value("1"), 618 | ) 619 | .subcommand(SubCommand::with_name("export").about("Run an export instead of backing up")) 620 | .get_matches(); 621 | 622 | let config_file = matches.value_of("config").unwrap(); 623 | let workers: usize = matches.value_of("workers").unwrap().parse().unwrap(); 624 | println!("Using config file: {}", config_file); 625 | let config_str = fs::read_to_string(config_file)?; 626 | let config: GBackupConfig = toml::from_str(&config_str)?; 627 | println!("Loaded config from {}", config_file); 628 | config 629 | .accounts 630 | .into_iter() 631 | .map(|account| { 632 | if let Some(_) = matches.subcommand_matches("export") { 633 | let runner = ExportRunner { account }; 634 | if let Err(error) = runner.run_export() { 635 | println!("Got error running export: {}", error); 636 | } 637 | } else { 638 | let runner = BackupRunner::new(account, workers); 639 | if let Err(error) = runner.run_backup() { 640 | println!("Got error running backup: {}", error); 641 | } 642 | } 643 | }) 644 | .for_each(drop); 645 | Ok(()) 646 | } 647 | --------------------------------------------------------------------------------