1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
|
This is an autogenerated patch header for a single-debian-patch file. The
delta against upstream is either kept as a single patch, or maintained
in some VCS, and exported as a single patch instead of more manageable
atomic patches.
--- /dev/null
+++ gmailieer-1.3/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github: gauteh
+custom: ["https://www.paypal.me/gauteh"]
--- /dev/null
+++ gmailieer-1.3/.github/workflows/python-test.yml
@@ -0,0 +1,38 @@
+# This workflow will install Python dependencies, run tests and lint with a single version of Python
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Python tests
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+permissions: {}
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ persist-credentials: false
+ - name: Set up Python 3.9
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+ - name: Install notmuch
+ run: |
+ sudo apt-get install notmuch
+ sudo apt-get install libnotmuch-dev
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install pytest
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+ - name: Test with pytest
+ run: |
+ pytest -v tests
--- gmailieer-1.3.orig/COPYING.GPL-3.0+
+++ gmailieer-1.3/COPYING.GPL-3.0+
@@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -645,7 +645,7 @@ the "copyright" line and a pointer to wh
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@@ -664,11 +664,11 @@ might be different; for a GUI interface,
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
-<http://www.gnu.org/licenses/>.
+<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
-<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+<https://www.gnu.org/philosophy/why-not-lgpl.html>.
--- gmailieer-1.3.orig/LICENSE.md
+++ gmailieer-1.3/LICENSE.md
@@ -14,7 +14,7 @@
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
See COPYING.GPL-3.0+
--- gmailieer-1.3.orig/docs/index.md
+++ gmailieer-1.3/docs/index.md
@@ -27,7 +27,7 @@ While Lieer has been used to successfull
## Installation
-After cloning the repository Lieer can be installed through pip by using the command ```pip install .``
+After cloning the repository Lieer can be installed through pip by using the command ```pip install .```
# Usage
This assumes your root mail folder is in `~/.mail` and that this folder is _already_ set up with notmuch.
@@ -140,7 +140,7 @@ the `Subject:` header must also match, b
> your account, it seems like GMail resets the from to your account _address_
> only.
-Note that the following flags are ignored for `sendmail` compatability:
+Note that the following flags are ignored for `sendmail` compatibility:
- `-f` (ignored, set envelope `From:` yourself)
- `-o` (ignored)
@@ -162,7 +162,7 @@ Lieer can be configured using `gmi set`.
**`File extension`** is an optional argument to include the specified extension in local file names (e.g., `mbox`) which can be useful for indexing them with third-party programs.
-*Important:* If you change this setting after synchronizing, the best case scenario is that all files will apear to not have being pulled down and will be re-downloaded (and duplicated with a different extension in the maildir). There might also be changes to tags. You should in theory be able to change it by renaming all files, but since this will update the lastmod you will get a check on all files.
+*Important:* If you change this setting after synchronizing, the best case scenario is that all files will appear to not have being pulled down and will be re-downloaded (and duplicated with a different extension in the maildir). There might also be changes to tags. You should in theory be able to change it by renaming all files, but since this will update the lastmod you will get a check on all files.
**`Drop non existing labels`** can be used to silently ignore errors where GMail gives us a label identifier which is not associated with a label. See [Caveats](#caveats).
@@ -176,6 +176,14 @@ Lieer can be configured using `gmi set`.
*Important*: See note below on [changing this setting after initial sync](#changing-ignored-tags-and-translation-after-initial-sync).
+**`Local Trash Tag (local)`** can be used to set the local tag to which the remote GMail 'TRASH' label is translated.
+
+ *Important*: See note below on [changing this setting after initial sync](#changing-ignored-tags-and-translation-after-initial-sync).
+
+**`Translation List Overlay`** can be used to add or change entries in the translation mapping between local and remote tags. Argument is a comment-separated list with an even number of items. This is interpreted as a list of pairs of (remote, local), where each pair is added to the tag translation overwriting any existing translation for that tag if any. For example,
+`--translation-list-overlay CATEGORY_FORUMS,my_forum_tag` will translate Google's CATEGORY_FORUMS tag to my_forum_tag.')
+
+ *Important*: See note below on [changing this setting after initial sync](#changing-ignored-tags-and-translation-after-initial-sync).
## Changing ignored tags and translation after initial sync
@@ -187,11 +195,12 @@ Before changing either setting make sure
When changing the opposite setting: `--ignore-tags-local`, do a full push (dry-run first): `gmi push -f --dry-run`.
-The same goes for the option `--replace-slash-with-dot`. I prefer to do `gmi pull -f --dry-run` after changing this option. This will overwrite the local tags with the remote labels.
+The same goes for the options `--replace-slash-with-dot` and `--local-trash-tag`. I prefer to do `gmi pull -f --dry-run` after changing this option. This will overwrite the local tags with the remote labels.
+
# Translation between labels and tags
-We translate some of the GMail labels to other tags. The map of labels to tags are:
+We translate some of the GMail labels to other tags. The default map of labels to tags are:
```py
'INBOX' : 'inbox',
@@ -211,16 +220,22 @@ We translate some of the GMail labels to
'CATEGORY_FORUMS' : 'forums',
```
+The 'trash' local tag can be replaced using the `--local-trash-tag` option.
+
# Using your own API key
Lieer ships with an API key that is shared openly, this key shares API quota, but [cannot be used to access data](https://github.com/gauteh/lieer/pull/9) unless access is gained to your private `access_token` or `refresh_token`.
-You can get an [api key](https://console.developers.google.com/flows/enableapi?apiid=gmail) for a CLI application to use for yourself. Store the `client_secret.json` file somewhere safe and specify it to `gmi auth -c`. You can do this on a repository that is already initialized.
+You can get an [api key](https://console.developers.google.com/flows/enableapi?apiid=gmail) for a CLI application to use for yourself. Store the `client_secret.json` file somewhere safe and specify it to `gmi auth -c`. You can do this on a repository that is already initialized, possibly using `-f` to force reauthorizing with the new client secrets.
# Privacy policy
- Lieer downloads e-mail and labels to your local computer. No data is sent elsewhere.
+Lieer downloads e-mail and labels to your local computer. No data is sent elsewhere.
+
+Lieers use and transfer to any other app of information received from Google
+APIs will adhere to [Google API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes),
+including the Limited Use requirements
# Caveats
@@ -232,7 +247,7 @@ bug](https://issuetracker.google.com/iss
* The `draft` and `sent` labels are read only: They are synced from GMail to local notmuch tags, but not back (if you change them via notmuch).
* [Only one of the tags](https://github.com/gauteh/lieer/issues/26) `inbox`, `spam`, and `trash` may be added to an email. For
-the time being, `trash` will be prefered over `spam`, and `spam` over `inbox`.
+the time being, `trash` will be preferred over `spam`, and `spam` over `inbox`.
* `Trash` (capital `T`) is reserved and not allowed, use `trash` (lowercase, see above) to bin messages remotely.
--- gmailieer-1.3.orig/lieer/gmailieer.py
+++ gmailieer-1.3/lieer/gmailieer.py
@@ -22,7 +22,8 @@ import os, sys
import argparse
from oauth2client import tools
import googleapiclient
-import notmuch
+import googleapiclient.errors
+import notmuch2
from .remote import *
from .local import *
@@ -98,19 +99,19 @@ class Gmailieer:
parser_send.add_argument ('-d', '--dry-run', action='store_true',
default = False, help = 'do not actually send message')
- # Ignored arguments for sendmail compatability
+ # Ignored arguments for sendmail compatibility
if '-oi' in sys.argv:
sys.argv.remove('-oi')
if '-i' in sys.argv:
sys.argv.remove('-i')
- parser_send.add_argument('-i', action='store_true', default = None, help = 'Ignored: always implied, allowed for sendmail compatability.', dest = 'i3')
+ parser_send.add_argument('-i', action='store_true', default = None, help = 'Ignored: always implied, allowed for sendmail compatibility.', dest = 'i3')
parser_send.add_argument('-t', '--read-recipients', action='store_true',
default = False, dest = 'read_recipients',
help = 'Read recipients from message headers. This is always done by GMail. If this option is not specified, the same addresses (as those in the headers) must be specified as additional arguments.')
- parser_send.add_argument('-f', type = str, help = 'Ignored: has no effect, allowed for sendmail compatability.', dest = 'i1')
+ parser_send.add_argument('-f', type = str, help = 'Ignored: has no effect, allowed for sendmail compatibility.', dest = 'i1')
parser_send.add_argument('recipients', nargs = '*', default = [],
help = 'Recipients to send this message to (these are essentially ignored, but they are validated against the header fields.)')
@@ -144,6 +145,17 @@ class Gmailieer:
parser_auth.add_argument ('-f', '--force', action = 'store_true',
default = False, help = 'Re-authorize')
+ # These are taken from oauth2lib/tools.py for compatibility with its
+ # run_flow() method used during oauth
+ parser_auth.add_argument('--auth-host-name', default='localhost',
+ help='Hostname when running a local web server')
+ parser_auth.add_argument('--auth-host-port', default=[8080, 8090], type=int,
+ nargs='*',
+ help='Port web server should listen on')
+ parser_auth.add_argument('--noauth_local_webserver', action='store_true',
+ default=False,
+ help='Do not run a local web server (no longer supported by Google)')
+
parser_auth.set_defaults (func = self.authorize)
# init
@@ -200,6 +212,12 @@ class Gmailieer:
parser_set.add_argument ('--no-remove-local-messages', action = 'store_true', default = False,
help = 'Do not remove messages that have been deleted on the remote')
+ parser_set.add_argument ('--local-trash-tag', type = str, default = None,
+ help = 'The local tag to use for the remote label TRASH.')
+
+ parser_set.add_argument ('--translation-list-overlay', type = str, default = None,
+ help = 'A list with an even number of items representing a list of pairs of (remote, local), where each pair is added to the tag translation.')
+
parser_set.set_defaults (func = self.set)
@@ -305,20 +323,17 @@ class Gmailieer:
self.remote.get_labels ()
# loading local changes
- with notmuch.Database () as db:
- (rev, uuid) = db.get_revision ()
+ with notmuch2.Database() as db:
+ rev = db.revision().rev
if rev == self.local.state.lastmod:
self.vprint ("push: everything is up-to-date.")
return
qry = "path:%s/** and lastmod:%d..%d" % (self.local.nm_relative, self.local.state.lastmod, rev)
- query = notmuch.Query (db, qry)
- total = query.count_messages () # probably destructive here as well
- query = notmuch.Query (db, qry)
+ messages = [db.get(m.path) for m in db.messages(qry)]
- messages = list(query.search_messages ())
if self.limit is not None and len(messages) > self.limit:
messages = messages[:self.limit]
@@ -358,7 +373,7 @@ class Gmailieer:
self.bar_create (leave = True, total = len(actions), desc = 'pushing, 0 changed')
changed = 0
- def cb (resp):
+ def cb (_):
nonlocal changed
self.bar_update (1)
changed += 1
@@ -552,7 +567,7 @@ class Gmailieer:
changed = True
if self.local.config.remove_local_messages and len(deleted_messages) > 0:
- with notmuch.Database (mode = notmuch.Database.MODE.READ_WRITE) as db:
+ with notmuch2.Database(mode = notmuch2.Database.MODE.READ_WRITE) as db:
for m in tqdm (deleted_messages, leave = True, desc = 'removing messages'):
self.local.remove (m['id'], db)
@@ -560,7 +575,7 @@ class Gmailieer:
if len (labels_changed) > 0:
lchanged = 0
- with notmuch.Database (mode = notmuch.Database.MODE.READ_WRITE) as db:
+ with notmuch2.Database(mode = notmuch2.Database.MODE.READ_WRITE) as db:
self.bar_create (total = len(labels_changed), leave = True, desc = 'updating tags (0)')
for m in labels_changed:
r = self.local.update_tags (m, None, db)
@@ -644,7 +659,7 @@ class Gmailieer:
all_local = set(self.local.gids.keys())
remove = list(all_local - all_remote)
self.bar_create (leave = True, total = len(remove), desc = 'removing deleted')
- with notmuch.Database (mode = notmuch.Database.MODE.READ_WRITE) as db:
+ with notmuch2.Database (mode = notmuch2.Database.MODE.READ_WRITE) as db:
for m in remove:
self.local.remove(m, db)
self.bar_update (1)
@@ -668,8 +683,8 @@ class Gmailieer:
# set notmuch lastmod time, since we have now synced everything from remote
# to local
- with notmuch.Database() as db:
- (rev, uuid) = db.get_revision()
+ with notmuch2.Database() as db:
+ rev = db.revision().rev
if not self.dry_run:
self.local.state.set_lastmod(rev)
@@ -709,7 +724,7 @@ class Gmailieer:
# opening db for whole metadata sync
def _got_msgs (ms):
- with notmuch.Database (mode = notmuch.Database.MODE.READ_WRITE) as db:
+ with notmuch2.Database(mode = notmuch2.Database.MODE.READ_WRITE) as db:
for m in ms:
self.bar_update (1)
self.local.update_tags (m, None, db)
@@ -731,7 +746,7 @@ class Gmailieer:
Returns:
list of messages which were updated, these have also been updated in Notmuch and
- does not need to be partially upated.
+ does not need to be partially updated.
"""
@@ -743,7 +758,7 @@ class Gmailieer:
def _got_msgs (ms):
# opening db per message batch since it takes some time to download each one
- with notmuch.Database (mode = notmuch.Database.MODE.READ_WRITE) as db:
+ with notmuch2.Database(mode = notmuch2.Database.MODE.READ_WRITE) as db:
for m in ms:
self.bar_update (1)
self.local.store (m, db)
@@ -812,11 +827,14 @@ class Gmailieer:
if 'In-Reply-To' in eml:
repl = eml['In-Reply-To'].strip().strip('<>')
self.vprint("looking for original message: %s" % repl)
- with notmuch.Database (mode = notmuch.Database.MODE.READ_ONLY) as db:
- nmsg = db.find_message(repl)
+ with notmuch2.Database(mode = notmuch2.Database.MODE.READ_ONLY) as db:
+ try:
+ nmsg = db.find(repl)
+ except LookupError:
+ nmsg = None
if nmsg is not None:
(_, gids) = self.local.messages_to_gids([nmsg])
- if nmsg.get_header('Subject') != eml['Subject']:
+ if nmsg.header('Subject') != eml['Subject']:
self.vprint ("warning: subject does not match, might not be able to associate with existing thread.")
if len(gids) > 0:
@@ -875,6 +893,12 @@ class Gmailieer:
if args.file_extension is not None:
self.local.config.set_file_extension (args.file_extension)
+ if args.local_trash_tag is not None:
+ self.local.config.set_local_trash_tag (args.local_trash_tag)
+
+ if args.translation_list_overlay is not None:
+ self.local.config.set_translation_list_overlay (args.translation_list_overlay)
+
print ("Repository information and settings:")
print ("Account ...........: %s" % self.local.config.account)
print ("historyId .........: %d" % self.local.state.last_historyId)
@@ -887,6 +911,8 @@ class Gmailieer:
print ("Replace . with / ..........:", self.local.config.replace_slash_with_dot)
print ("Ignore tags (local) .......:", self.local.config.ignore_tags)
print ("Ignore labels (remote) ....:", self.local.config.ignore_remote_labels)
+ print ("Trash tag (local) .........:", self.local.config.local_trash_tag)
+ print ("Translation list overlay ..:", self.local.config.translation_list_overlay)
def vprint (self, *args, **kwargs):
"""
--- gmailieer-1.3.orig/lieer/local.py
+++ gmailieer-1.3/lieer/local.py
@@ -22,7 +22,7 @@ import configparser
from pathlib import Path
import tempfile
-import notmuch
+import notmuch2
from .remote import Remote
class Local:
@@ -31,7 +31,7 @@ class Local:
# NOTE: Update README when changing this map.
- translate_labels = {
+ translate_labels_default = {
'INBOX' : 'inbox',
'SPAM' : 'spam',
'TRASH' : 'trash',
@@ -49,7 +49,7 @@ class Local:
'CATEGORY_FORUMS' : 'forums',
}
- labels_translate = { v: k for k, v in translate_labels.items () }
+ labels_translate_default = { v: k for k, v in translate_labels_default.items () }
ignore_labels = set ([
'archive',
@@ -66,6 +66,35 @@ class Local:
'voicemail',
])
+ def update_translation(self, remote, local):
+ """
+ Convenience function to ensure both maps (remote -> local and local -> remote)
+ get updated when you update a translation.
+ """
+ # Did you reverse the parameters?
+ assert remote in self.translate_labels
+ self.translate_labels[remote] = local
+ self.labels_translate = { v: k for k, v in self.translate_labels.items () }
+
+ def update_translation_list_with_overlay(self, translation_list_overlay):
+ """
+ Takes a list with an even number of items. The list is interpreted as a list of pairs
+ of (remote, local), where each member of each pair is a string. Each pair is added to the
+ translation, overwriting the translation if one already exists (in either direction).
+ If either the remote or the local labels are non-unique, the later items in the list will
+ overwrite the earlier ones in the direction in which the source is non-unique (for example,
+ ["a", "1", "b", 2", "a", "3"] will yield {'a': 3, 'b': 2} in one direction and {1: 'a', 2: 'b', 3: 'a'}
+ in the other).
+ """
+
+ if len(translation_list_overlay) % 2 != 0:
+ raise Exception(f'Translation list overlay must have an even number of items: {translation_list_overlay}')
+
+ for i in range(0,len(translation_list_overlay),2):
+ (remote, local) = translation_list_overlay[i], translation_list_overlay[i+1]
+ self.translate_labels[remote] = local
+ self.labels_translate[local] = remote
+
class RepositoryException (Exception):
pass
@@ -80,6 +109,8 @@ class Local:
ignore_remote_labels = None
remove_local_messages = True
file_extension = None
+ local_trash_tag = 'trash'
+ translation_list_overlay = None
def __init__ (self, config_f):
self.config_f = config_f
@@ -103,6 +134,8 @@ class Local:
self.ignore_tags = set(self.json.get ('ignore_tags', []))
self.ignore_remote_labels = set(self.json.get ('ignore_remote_labels', Remote.DEFAULT_IGNORE_LABELS))
self.file_extension = self.json.get ('file_extension', '')
+ self.local_trash_tag = self.json.get ('local_trash_tag', 'trash')
+ self.translation_list_overlay = self.json.get ('translation_list_overlay', [])
def write (self):
self.json = {}
@@ -116,6 +149,8 @@ class Local:
self.json['ignore_remote_labels'] = list(self.ignore_remote_labels)
self.json['remove_local_messages'] = self.remove_local_messages
self.json['file_extension'] = self.file_extension
+ self.json['local_trash_tag'] = self.local_trash_tag
+ self.json['translation_list_overlay'] = self.translation_list_overlay
if os.path.exists (self.config_f):
shutil.copyfile (self.config_f, self.config_f + '.bak')
@@ -166,7 +201,7 @@ class Local:
def set_file_extension (self, t):
try:
- with tempfile.NamedTemporaryFile (dir = os.path.dirname (self.state_f), suffix = t) as fd:
+ with tempfile.NamedTemporaryFile (dir = os.path.dirname (self.config_f), suffix = t) as _:
pass
self.file_extension = t.strip ()
@@ -175,6 +210,23 @@ class Local:
print ("Failed creating test file with file extension: " + t + ", not set.")
raise
+ def set_local_trash_tag (self, t):
+ if ',' in t:
+ print('The local_trash_tag must be a single tag, not a list. Commas are not allowed.')
+ raise ValueError()
+ self.local_trash_tag = t.strip() or 'trash'
+ self.write()
+
+ def set_translation_list_overlay (self, t):
+ if len(t.strip ()) == 0:
+ self.translation_list_overlay = []
+ else:
+ self.translation_list_overlay = [ tt.strip () for tt in t.split(',') ]
+ if len(self.translation_list_overlay) % 2 != 0:
+ raise Exception(f'Translation list overlay must have an even number of items: {self.translation_list_overlay}')
+ self.write ()
+
+
class State:
# last historyid of last synchronized message, anything that has happened
@@ -241,7 +293,7 @@ class Local:
self.lastmod = m
self.write ()
-
+ # we are in the class "Local"; this is the Local instance constructor
def __init__ (self, g):
self.gmailieer = g
self.wd = os.getcwd ()
@@ -255,6 +307,10 @@ class Local:
# mail store
self.md = os.path.join (self.wd, 'mail')
+ # initialize label translation instance variables
+ self.translate_labels = Local.translate_labels_default.copy()
+ self.labels_translate = Local.labels_translate_default.copy()
+
def load_repository (self, block = False):
"""
Loads the current local repository
@@ -269,25 +325,13 @@ class Local:
for mail_dir in ('cur', 'new', 'tmp')]):
raise Local.RepositoryException ('local repository not initialized: could not find mail dir structure')
- self.config = Local.Config (self.config_f)
- self.state = Local.State (self.state_f, self.config)
-
- self.ignore_labels = self.ignore_labels | self.config.ignore_tags
-
## Check if we are in the notmuch db
- with notmuch.Database () as db:
+ with notmuch2.Database () as db:
try:
- self.nm_dir = db.get_directory (os.path.abspath(self.md))
- if self.nm_dir is not None:
- self.nm_dir = self.nm_dir.path
- else:
- # probably empty dir
- self.nm_dir = os.path.abspath (self.md)
-
- self.nm_relative = self.nm_dir[len(db.get_path ())+1:]
-
- except notmuch.errors.FileError:
+ self.nm_relative=str(Path(self.md).relative_to(db.path))
+ except ValueError:
raise Local.RepositoryException ("local mail repository not in notmuch db")
+ self.nm_dir=str(Path(self.md).resolve())
## Lock repository
try:
@@ -299,17 +343,19 @@ class Local:
except OSError:
raise Local.RepositoryException ("failed to lock repository (probably in use by another gmi instance)")
+ self.config = Local.Config (self.config_f)
+ self.state = Local.State (self.state_f, self.config)
+
+ self.ignore_labels = self.ignore_labels | self.config.ignore_tags
+ self.update_translation('TRASH', self.config.local_trash_tag)
+ self.update_translation_list_with_overlay(self.config.translation_list_overlay)
+
self.__load_cache__ ()
# load notmuch config
- cfg = os.environ.get('NOTMUCH_CONFIG', os.path.expanduser('~/.notmuch-config'))
- if not os.path.exists (cfg):
- raise Local.RepositoryException("could not find notmuch-config: %s" % cfg)
-
- self.nmconfig = configparser.ConfigParser ()
- self.nmconfig.read (cfg)
- self.new_tags = self.nmconfig['new']['tags'].split (';')
- self.new_tags = [t.strip () for t in self.new_tags if len(t.strip()) > 0]
+ with notmuch2.Database() as db:
+ self.new_tags = db.config.get("new.tags", "").split(';')
+ self.new_tags = [t.strip() for t in self.new_tags if len(t.strip()) > 0]
self.loaded = True
@@ -319,12 +365,12 @@ class Local:
## this cache is used to know which messages we have a physical copy of.
## hopefully this won't grow too gigantic with lots of messages.
self.files = []
- for (dp, dirnames, fnames) in os.walk (os.path.join (self.md, 'cur')):
+ for (_, _, fnames) in os.walk (os.path.join (self.md, 'cur')):
_fnames = ( 'cur/' + f for f in fnames )
self.files.extend (_fnames)
break
- for (dp, dirnames, fnames) in os.walk (os.path.join (self.md, 'new')):
+ for (_, _, fnames) in os.walk (os.path.join (self.md, 'new')):
_fnames = ( 'new/' + f for f in fnames )
self.files.extend (_fnames)
break
@@ -370,7 +416,7 @@ class Local:
"""
Update cache with filenames from nmsg, removing the old:
- nmsg - NotmuchMessage
+ nmsg - notmuch2.Message
old - tuple of old gid and old fname
"""
@@ -383,7 +429,8 @@ class Local:
self.gids.pop (old_gid)
# add message to cache
- for _f in nmsg.get_filenames ():
+ fname_iter = nmsg.filenames ()
+ for _f in fname_iter:
if self.contains (_f):
new_f = Path (_f)
@@ -404,7 +451,7 @@ class Local:
messages = []
for m in msgs:
- for fname in m.get_filenames ():
+ for fname in m.filenames ():
if self.contains (fname):
# get gmail id
gid = self.__filename_to_gid__ (os.path.basename (fname))
@@ -428,7 +475,7 @@ class Local:
return None
def __make_maildir_name__ (self, m, labels):
- # http://cr.yp.to/proto/maildir.html
+ # https://cr.yp.to/proto/maildir.html
ext = ''
if self.config.file_extension:
ext = '.' + self.config.file_extension
@@ -468,13 +515,16 @@ class Local:
return
fname = os.path.join (self.md, fname)
- nmsg = db.find_message_by_filename (fname)
+ try:
+ nmsg = db.get(fname)
+ except LookupError:
+ nmsg = None
if self.dry_run:
print ("(dry-run) deleting %s: %s." % (gid, fname))
else:
if nmsg is not None:
- db.remove_message (fname)
+ db.remove(fname)
os.unlink (fname)
self.files.remove (ffname)
@@ -507,7 +557,7 @@ class Local:
raise Local.RepositoryException ("local file already exists: %s" % p)
if os.path.exists (tmp_p):
- raise Local.RepositoryException ("local file already exists: %s" % p)
+ raise Local.RepositoryException ("local temporary file already exists: %s" % tmp_p)
if not self.dry_run:
with open (tmp_p, 'wb') as fd:
@@ -569,57 +619,52 @@ class Local:
print ("done.")
if not os.path.exists (fname):
- raise Local.RepositoryException ("tried to update tags on non-existant file: %s" % fname)
+ raise Local.RepositoryException ("tried to update tags on non-existent file: %s" % fname)
else:
- print ("(dry-run) tried to update tags on non-existant file: %s" % fname)
+ print ("(dry-run) tried to update tags on non-existent file: %s" % fname)
- nmsg = db.find_message_by_filename (fname)
+ try:
+ nmsg = db.get(fname)
+ except LookupError:
+ nmsg = None
if nmsg is None:
if self.dry_run:
print ("(dry-run) adding message: %s: %s, with tags: %s" % (gid, fname, str(labels)))
else:
try:
- if hasattr (notmuch.Database, 'index_file'):
- (nmsg, stat) = db.index_file (fname, True)
- else:
- (nmsg, stat) = db.add_message (fname, True)
- except notmuch.errors.FileNotEmailError:
+ (nmsg, _) = db.add (fname, sync_flags = True)
+ except notmuch2.FileNotEmailError:
print('%s is not an email' % fname)
return True
- nmsg.freeze ()
# adding initial tags
- for t in labels:
- nmsg.add_tag (t, True)
+ with nmsg.frozen():
+ for t in labels:
+ nmsg.tags.add (t)
- for t in self.new_tags:
- nmsg.add_tag (t, True)
+ for t in self.new_tags:
+ nmsg.tags.add (t)
- nmsg.thaw ()
- nmsg.tags_to_maildir_flags ()
+ nmsg.tags.to_maildir_flags()
self.__update_cache__ (nmsg)
return True
else:
# message is already in db, set local tags to match remote tags
- otags = set(nmsg.get_tags ())
+ otags = nmsg.tags
igntags = otags & self.ignore_labels
otags = otags - self.ignore_labels # remove ignored tags while checking
if otags != set (labels):
labels.extend (igntags) # add back local ignored tags before adding
if not self.dry_run:
- nmsg.freeze ()
-
- nmsg.remove_all_tags ()
- for t in labels:
- nmsg.add_tag (t, False)
-
- nmsg.thaw ()
-
- nmsg.tags_to_maildir_flags ()
+ with nmsg.frozen():
+ nmsg.tags.clear()
+ for t in labels:
+ nmsg.tags.add (t)
+ nmsg.tags.to_maildir_flags()
self.__update_cache__ (nmsg, (gid, fname))
else:
--- gmailieer-1.3.orig/lieer/remote.py
+++ gmailieer-1.3/lieer/remote.py
@@ -333,16 +333,16 @@ class Remote:
try:
batch.execute (http = self.http)
- # gradually reduce user delay if we had 10 ok batches
+ # gradually reduce user delay upon every ok batch
user_rate_ok += 1
- if user_rate_delay > 0 and user_rate_ok > 10:
+ if user_rate_delay > 0 and user_rate_ok > 0:
user_rate_delay = user_rate_delay // 2
print ("remote: decreasing delay to %s" % user_rate_delay)
user_rate_ok = 0
- # gradually increase batch request size if we had 10 ok requests
+ # gradually increase batch request size upon every ok request
req_ok += 1
- if max_req < self.BATCH_REQUEST_SIZE and req_ok > 10:
+ if max_req < self.BATCH_REQUEST_SIZE and req_ok > 0:
max_req = min (max_req * 2, self.BATCH_REQUEST_SIZE)
print ("remote: increasing batch request size to: %d" % max_req)
req_ok = 0
@@ -488,8 +488,8 @@ class Remote:
gid = gmsg['id']
found = False
- for f in nmsg.get_filenames ():
- if gid in f:
+ for f in nmsg.filenames():
+ if gid in str(f):
found = True
# this can happen if a draft is edited remotely and is synced before it is sent. we'll
@@ -528,7 +528,7 @@ class Remote:
labels = set(labels)
# current tags
- tags = set(nmsg.get_tags ())
+ tags = nmsg.tags
# remove special notmuch tags
tags = tags - self.gmailieer.local.ignore_labels
@@ -606,7 +606,7 @@ class Remote:
Push label changes
"""
max_req = self.BATCH_REQUEST_SIZE
- N = len (actions)
+ N = len(actions)
i = 0
j = 0
@@ -615,19 +615,19 @@ class Remote:
# How many requests with the current delay returned ok.
user_rate_ok = 0
- def _cb (rid, resp, excep):
+ def _cb(rid, resp, excep):
nonlocal j
if excep is not None:
if type(excep) is googleapiclient.errors.HttpError and excep.resp.status == 404:
# message could not be found this is probably a deleted message, spam or draft
# message since these are not included in the messages.get() query by default.
- print ("remote: could not find remote message: %s!" % gids[j])
+ print ("remote: could not find remote message: %s!" % resp)
j += 1
return
elif type(excep) is googleapiclient.errors.HttpError and excep.resp.status == 400:
# message id invalid, probably caused by stray files in the mail repo
- print ("remote: message id: %s is invalid! are there any non-lieer files created in the lieer repository?" % gids[j])
+ print ("remote: message id is invalid! are there any non-lieer files created in the lieer repository? %s" % resp)
j += 1
return
@@ -639,16 +639,16 @@ class Remote:
else:
j += 1
- cb (resp)
+ cb(resp)
while i < N:
n = 0
j = i
- batch = self.service.new_batch_http_request (callback = _cb)
+ batch = self.service.new_batch_http_request(callback = _cb)
while n < max_req and i < N:
a = actions[i]
- batch.add (a)
+ batch.add(a)
n += 1
i += 1
@@ -666,14 +666,14 @@ class Remote:
user_rate_delay = user_rate_delay // 2
user_rate_ok = 0
- except Remote.UserRateException as ex:
+ except Remote.UserRateException:
user_rate_delay = user_rate_delay * 2 + 1
print ("remote: user rate error, increasing delay to %s" % user_rate_delay)
user_rate_ok = 0
i = j # reset
- except Remote.BatchException as ex:
+ except Remote.BatchException:
if max_req > self.MIN_BATCH_REQUEST_SIZE:
max_req = max_req / 2
i = j # reset
--- gmailieer-1.3.orig/requirements.txt
+++ gmailieer-1.3/requirements.txt
@@ -1,4 +1,4 @@
google-api-python-client
oauth2client
tqdm
-notmuch
+notmuch2
--- gmailieer-1.3.orig/setup.py
+++ gmailieer-1.3/setup.py
@@ -69,7 +69,7 @@ setup(
# your project is installed. For an analysis of "install_requires" vs pip's
# requirements files see:
# https://packaging.python.org/en/latest/requirements.html
- install_requires=['oauth2client', 'google-api-python-client', 'tqdm', 'notmuch'],
+ install_requires=['oauth2client', 'google-api-python-client', 'tqdm', 'notmuch2'],
# List additional groups of dependencies here (e.g. development
# dependencies). You can install these using the following syntax,
@@ -89,7 +89,7 @@ setup(
# Although 'package_data' is the preferred approach, in some case you may
# need to place data files outside of your packages. See:
- # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
+ # https://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
# In this case, 'data_file' will be installed into '<sys.prefix>/my_data'
# data_files=[('my_data', ['data/data_file'])],
--- /dev/null
+++ gmailieer-1.3/tests/__init__.py
@@ -0,0 +1,18 @@
+import pytest
+import tempfile
+
+class MockGmi:
+ dry_run = False
+
+ def __init__(self):
+ pass
+
+
+
+@pytest.fixture
+def gmi():
+ """
+ Test gmi
+ """
+
+ return MockGmi()
--- /dev/null
+++ gmailieer-1.3/tests/test_local.py
@@ -0,0 +1,14 @@
+from . import *
+import lieer
+
+
+def test_update_translation_list(gmi):
+ l = lieer.Local(gmi)
+ l.update_translation_list_with_overlay(['a', '1', 'b', '2'])
+ assert l.translate_labels['a'] == '1'
+ assert l.translate_labels['b'] == '2'
+ assert l.labels_translate['1'] == 'a'
+ assert l.labels_translate['2'] == 'b'
+
+ with pytest.raises(Exception):
+ l.update_translation_list_with_overlay(['a', '1', 'b', '2', 'c'])
|