Skip to content

Commit

Permalink
Merge pull request #23 from Schluggi/1.0.2
Browse files Browse the repository at this point in the history
1.0.2
  • Loading branch information
Schluggi authored Oct 15, 2021
2 parents 40b0675 + 3acbb56 commit acec75f
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 26 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ 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
You want to merge `INBOX.Send Items` with the `INBOX.Send` folder? You can do this with `-r`/`--redirect`.
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
Expand Down
2 changes: 1 addition & 1 deletion imapidle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
79 changes: 58 additions & 21 deletions pymap-copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,29 @@


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.')
return 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',
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -88,6 +105,20 @@ def login(client, user, password):
return False, f'{colorize("Error:", color="red", bold=True)} No active connection'


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)
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",
Expand All @@ -106,7 +137,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, '
Expand Down Expand Up @@ -156,10 +187,12 @@ def login(client, user, password):
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
destination_delimiter, source_delimiter = None, None
destination_separator, source_separator = None, None
db = {
'source': {
'folders': {}
Expand Down Expand Up @@ -194,7 +227,6 @@ def login(client, user, password):
if args.denied_flags:
denied_flags.extend([f'\\{flag}'.encode() for flag in args.denied_flags.lower().split(',')])


print()

#: connecting source
Expand Down Expand Up @@ -238,20 +270,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')
Expand Down Expand Up @@ -281,9 +312,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:
Expand Down Expand Up @@ -350,10 +382,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:
Expand Down Expand Up @@ -393,13 +426,16 @@ def login(client, user, password):
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, '
Expand All @@ -422,6 +458,7 @@ def login(client, user, password):
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:
Expand All @@ -448,13 +485,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:
Expand Down
2 changes: 1 addition & 1 deletion utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit acec75f

Please sign in to comment.