From be1e8583afe1d2c2655a97c1f2545af21ed29211 Mon Sep 17 00:00:00 2001 From: Lukas Schulte-Tickmann Date: Tue, 12 Jan 2021 20:50:54 +0100 Subject: [PATCH 1/9] fixed: idle help typo --- pymap-copy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymap-copy.py b/pymap-copy.py index eb5747a..b9084dd 100755 --- a/pymap-copy.py +++ b/pymap-copy.py @@ -106,7 +106,7 @@ def login(client, user, password): parser.add_argument('--denied-flags', help='mails with this flags will be skipped', type=str) parser.add_argument('-r', '--redirect', help='redirect a folder (source:destination --denied-flags seen,recent -d)', action='append') -parser.add_argument('--idle-interval', help='time in defines the interval (in seconds) in which the idle process is ' +parser.add_argument('--idle-interval', help='defines the interval (in seconds) after that the idle process is ' 'restarted (default: 1680)', type=int, default=1680) parser.add_argument('--ignore-quota', help='ignores insufficient quota', action='store_true') parser.add_argument('--ignore-folder-flags', help='do not link default IMAP folders automatically (like Drafts, ' From f9612a93ddd7e5f4d68e265b1583dc566c34d0ec Mon Sep 17 00:00:00 2001 From: Lukas Schulte-Tickmann Date: Wed, 13 Jan 2021 13:28:12 +0100 Subject: [PATCH 2/9] fixed: missing link to issue #8 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 749f3ad..a29026d 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ If you just want to look what would happen append `-d`/`--dry-run`. ### Incorrect login If your password has special characters (like `!`, `$`, `#`, ...) in it, you have to quote them with a backslash (`\`) -in front. This is a common mistake (#8). +in front. This is a common mistake ([#8](https://github.com/Schluggi/pymap-copy/issues/8)). ### Redirections and destination root #### Redirections From 2d75c7d082351fc1f96cb374d14a116909b9c6be Mon Sep 17 00:00:00 2001 From: Lukas Schulte-Tickmann Date: Wed, 13 Jan 2021 15:30:28 +0100 Subject: [PATCH 3/9] fixed: index error if the mailbox is lager than 1000 GiB --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index c7d624f..485d4cc 100644 --- a/utils.py +++ b/utils.py @@ -22,7 +22,7 @@ def beautysized(b, factor=1000, precision=1): rv = 0 if factor > b >= 0: - rv = '{} {}'.format(b, units[0]) + rv = f'{b} {units[0]}' elif b < factor**2: rv = template.format(b/factor, units[1]) elif b < factor**3: From ce0a4be87689995300e5a3c90d7c172d5d684be5 Mon Sep 17 00:00:00 2001 From: Lukas Schulte-Tickmann Date: Wed, 13 Jan 2021 15:34:17 +0100 Subject: [PATCH 4/9] changed: renaming delimiter in separator --- pymap-copy.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pymap-copy.py b/pymap-copy.py index b9084dd..5fe3bf7 100755 --- a/pymap-copy.py +++ b/pymap-copy.py @@ -159,7 +159,7 @@ def login(client, user, password): SPECIAL_FOLDER_FLAGS = [b'\\Archive', b'\\Junk', b'\\Drafts', b'\\Trash', b'\\Sent'] denied_flags = [b'\\recent'] progress = 0 -destination_delimiter, source_delimiter = None, None +destination_separator, source_separator = None, None db = { 'source': { 'folders': {} @@ -281,9 +281,10 @@ def login(client, user, password): #: get source folders print(colorize('Getting source folders : loading (this can take a while)', clear=True), flush=True, end='') -for flags, delimiter, name in source.list_folders(): - if not source_delimiter: - source_delimiter = delimiter.decode() +logging.info('Getting source folders (this can take a while)') +for flags, separator, name in source.list_folders(): + if not source_separator: + source_separator = separator.decode() if args.source_folder: if name not in args.source_folder and name.startswith(wildcards) is False: @@ -350,10 +351,11 @@ def login(client, user, password): #: get destination folders print(colorize('Getting destination folders : loading (this can take a while)', clear=True), flush=True, end='') -for flags, delimiter, name in destination.list_folders(args.destination_root): +logging.info('Getting destination folders (this can take a while)') +for flags, separator, name in destination.list_folders(args.destination_root): - if not destination_delimiter: - destination_delimiter = delimiter.decode() + if not destination_separator: + destination_separator = separator.decode() #: no need to process the source destination mailbox if we skipped the source for it if args.source_folder: @@ -448,13 +450,13 @@ def login(client, user, password): try: for sf_name in sorted(db['source']['folders'], key=lambda x: x.lower()): source.select_folder(sf_name, readonly=True) - df_name = sf_name.replace(source_delimiter, destination_delimiter) + df_name = sf_name.replace(source_separator, destination_separator) if args.destination_root: if args.destination_root_merge is False or \ - (df_name.startswith(f'{args.destination_root}{destination_delimiter}') is False + (df_name.startswith(f'{args.destination_root}{destination_separator}') is False and df_name != args.destination_root): - df_name = f'{args.destination_root}{destination_delimiter}{df_name}' + df_name = f'{args.destination_root}{destination_separator}{df_name}' #: link special IMAP folder if not args.ignore_folder_flags: From 1386913e2826745260244ad987f70eefdaf5dca4 Mon Sep 17 00:00:00 2001 From: Lukas Schulte-Tickmann Date: Wed, 13 Jan 2021 15:57:39 +0100 Subject: [PATCH 5/9] changed: renaming delimiter in separator --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a29026d..00b8296 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ You want to merge `INBOX.Send Items` with the `INBOX.Send` folder? You can do th The syntax of this argument is simple `source:destination`. For this example you can use `-r "INBOX.Send Items:INBOX.Send"` to put all mails from the source folder `INBOX.Send Items` the to destination folder `INBOX.Send`. Please make sure you use quotation marks if one of the folders includes a special character or space like -as in this example. In addition, the folder names must be case-sensitive with the correct delimiter. Do a dry run +as in this example. In addition, the folder names must be case-sensitive with the correct seperator. Do a dry run (`-d`/`--dry-run`) to check that everything will redirect correctly. #### Destination root From e7881f3dbc5a1cbefd2dbfbbe2713c8650ce0b0d Mon Sep 17 00:00:00 2001 From: Lukas Schulte-Tickmann Date: Wed, 13 Jan 2021 16:26:42 +0100 Subject: [PATCH 6/9] improved: move quota calculation into an extra function --- pymap-copy.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pymap-copy.py b/pymap-copy.py index 5fe3bf7..92c0e63 100755 --- a/pymap-copy.py +++ b/pymap-copy.py @@ -88,6 +88,17 @@ def login(client, user, password): return False, f'{colorize("Error:", color="red", bold=True)} No active connection' +def get_quota(client): + if client.has_capability('QUOTA') and args.ignore_quota is False: + quota = client.get_quota()[0] + quota_usage = beautysized(quota.usage * 1000) + quota_limit = beautysized(quota.limit * 1000) + quota_filled = f'{quota.usage / quota.limit * 100:.0f}' + return quota, quota_usage, quota_limit, quota_filled + logging.info(f'Server does not support quota') + return None, None, None, None + + parser = ArgumentParser(description='Copy and transfer IMAP mailboxes', epilog=f'pymap-copy by {__author__} ({__url__})') parser.add_argument('-v', '--version', help='show version and exit.', action="version", @@ -238,20 +249,19 @@ def login(client, user, password): #: get quota from source print('Getting source quota : ', end='', flush=True) -if source.has_capability('QUOTA') and args.ignore_quota is False: - source_quota = source.get_quota()[0] - print(f'{beautysized(source_quota.usage*1000)}/{beautysized(source_quota.limit*1000)} ' - f'({source_quota.usage / source_quota.limit * 100:.0f}%)') +logging.info(f'Getting source quota...') +source_quota, source_quota_usage, source_quota_limit, source_quota_filled = get_quota(source) +if source_quota: + print(f'{source_quota_usage}/{source_quota_limit} ({source_quota_filled}%)') else: - source_quota = None print('server does not support quota') #: get quota from destination print('Getting destination quota : ', end='', flush=True) -if destination.has_capability('QUOTA') and args.ignore_quota is False: - destination_quota = destination.get_quota()[0] - print(f'{beautysized(destination_quota.usage*1000)}/{beautysized(destination_quota.limit*1000)} ' - f'({destination_quota.usage / destination_quota.limit * 100:.0f}%)') +logging.info(f'Getting destination quota...') +destination_quota, destination_quota_usage, destination_quota_limit, destination_quota_filled = get_quota(destination) +if destination_quota: + print(f'{source_quota_usage}/{source_quota_limit} ({source_quota_filled}%)') else: destination_quota = None print('server does not support quota') From fadb658aef28b9514d716ea6cbd138f71b40353f Mon Sep 17 00:00:00 2001 From: Lukas Schulte-Tickmann Date: Wed, 13 Jan 2021 16:57:55 +0100 Subject: [PATCH 7/9] added: comments --- pymap-copy.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/pymap-copy.py b/pymap-copy.py index 92c0e63..5142a55 100755 --- a/pymap-copy.py +++ b/pymap-copy.py @@ -16,6 +16,10 @@ def check_encryption(value): + """ + check for the --???-encryption argument + raise an exception if the given encryption is invalid + """ value = value.lower() if value not in ['ssl', 'tls', 'starttls', 'none']: raise ArgumentTypeError(f'{value} is an unknown encryption. Use can use ssl, tls, starttls or none instead.') @@ -23,12 +27,18 @@ def check_encryption(value): def default_port(encryption): + """ + returns a port based on the encryption + """ if encryption in ['starttls', 'none']: return 143 return 993 def colorize(s, color=None, bold=False, clear=False): + """ + turn the string into a colored and/or bold one + """ colors = {'red': '\x1b[31m', 'green': '\x1b[32m', 'cyan': '\x1b[36m', @@ -46,6 +56,10 @@ def colorize(s, color=None, bold=False, clear=False): def connect(server, port, encryption): + """ + connect to the server with the right ssl_context in case of encryption + returns a client handle if connected and None if not + """ use_ssl = False ssl_context = None # IMAPClient will use a context by default if ssl_context is None @@ -78,6 +92,9 @@ def connect(server, port, encryption): def login(client, user, password): + """ + login the client with the given username and password + """ if client: try: client.login(user, password) @@ -89,6 +106,9 @@ def login(client, user, password): def get_quota(client): + """ + returns the quota of the mailbox + """ if client.has_capability('QUOTA') and args.ignore_quota is False: quota = client.get_quota()[0] quota_usage = beautysized(quota.usage * 1000) @@ -167,6 +187,8 @@ def get_quota(client): destination_port = default_port(args.destination_encryption) + +#: pre-defined variables SPECIAL_FOLDER_FLAGS = [b'\\Archive', b'\\Junk', b'\\Drafts', b'\\Trash', b'\\Sent'] denied_flags = [b'\\recent'] progress = 0 @@ -205,7 +227,6 @@ def get_quota(client): if args.denied_flags: denied_flags.extend([f'\\{flag}'.encode() for flag in args.denied_flags.lower().split(',')]) - print() #: connecting source @@ -405,13 +426,16 @@ def get_quota(client): print(f'({colorize("filtered by arguments", color="yellow")})', end='') print('\n') + #: list mode if args.list: + #: list all source folders print(colorize('Source:', bold=True)) for name in db['source']['folders']: print(f'{name} ({len(db["source"]["folders"][name]["mails"])} mails, ' f'{beautysized(db["source"]["folders"][name]["size"])})') + #: list all destination folders print(f'\n{colorize("Destination:", bold=True)}') for name in db['destination']['folders']: print(f'{name} ({len(db["destination"]["folders"][name]["mails"])} mails, ' @@ -434,6 +458,7 @@ def get_quota(client): try: r_source, r_destination = redirection.split(':', 1) + #: parsing wildcards if r_source.endswith('*'): wildcard_matches = [f for f in db['source']['folders'] if f.startswith(r_source[:-1])] if wildcard_matches: From 74563fd8d2fc3181d7269b1178fd9f2dc939c291 Mon Sep 17 00:00:00 2001 From: Lukas Schulte-Tickmann Date: Wed, 13 Jan 2021 17:04:40 +0100 Subject: [PATCH 8/9] fixed: zombie threads after killing --- imapidle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imapidle.py b/imapidle.py index baa7e96..dfaf2af 100644 --- a/imapidle.py +++ b/imapidle.py @@ -8,7 +8,7 @@ def __init__(self, client, interval=1680): self.interval = interval self._idle = False self._exit = False - super(IMAPIdle, self).__init__() + super(IMAPIdle, self).__init__(daemon=True) def run(self): while self._exit is False: From 3acbb5645f8d986b6d113533834e4132ace7157d Mon Sep 17 00:00:00 2001 From: Lukas Schulte-Tickmann Date: Fri, 15 Oct 2021 13:44:48 +0200 Subject: [PATCH 9/9] added: changelog for 1.0.2 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eab690c..611388b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ -## 1.1.0 (upcoming) +## 1.0.2 - support for python 3.5 dropped +- fixed: error at large quota ([#11](https://github.com/Schluggi/pymap-copy/issues/11)) +- fixed: no more zombie threads after killing ## 1.0.1 Introducing new version-system (major.minor.bugfix)