#!/usr/bin/python3
# imap-backup.py version 0.02, 2024-06-01
# Freeware by jek originally downloaded from ek.to or ek.ax
# This file have no warranty whatsoever, and may or may not work as intended.
# You may freely use, distribute or modify this file, or parts of it.
def do(cmd, *args):
	""" Perform an IMAP command, and return the data if the status is "OK".
	For any other status raise RuntimeError.
	"""
	flag, resp = cmd(*args)
	if flag != "OK":
		raise RuntimeError((flag, resp))
	return resp

def fin(ful):
	""" Escape a (file)name by replacing any non-ASCII character (and %)
	with a %hh token.
	"""
	return "".join(c if (32 <= ord(c) <= 126 and c != "%") else "%%%02x" % ord(c) for c in ful)

def trues():
	while True:
		yield True

def imupp(server, username, pwd=None, topfolder='.'):
	""" Backup emails from an IMAP server to local disk.
	Specify the server hostname and email username as arguments.
	Either specify password, or use None to get an input prompt.
	The local (top) folder can be specified, defaults to current folder.
	
	The emails are saved in one zip-file for each imap-folder, inside
	the username as the root folder. It will skip the trash folder.
	Each message is stored in .EML format, with it's UID as filename.
	If that UID already exists in the zip-file, it is not fetched again.
	(If a UID disapperas from the server, it is not deleted locally.)
	
	This script uses IMAP over SSL on the standard port 993.
	"""
	import re
	import os
	import ssl
	import imaplib
	import getpass
	import zipfile

	if pwd is None:
		pwd = getpass.getpass("Enter password for %s@%s: " % (username, server))
	
	context = ssl.create_default_context()	
	rebox = re.compile(rb'^\([^)]*\) "." (.*)$')
	got = had = old = kb = 0
	print("Connecting to %s@%s..." % (username, server))
	with imaplib.IMAP4_SSL(server, ssl_context=context) as M:
		M.login(username, pwd)
		boxes = do(M.list)
		folder = os.path.join(topfolder, fin(username))
		os.path.isdir(folder) or os.mkdir(folder)
		for box in boxes:
			boxname = rebox.match(box).group(1).decode('utf-8')
			if rb'\Trash' in box or rb'\Noselect' in box:
				print("Skip: %s" % (boxname,))
				continue
			
			do(M.select, boxname, True)
			uids = do(M.uid, "search", None, "ALL")[0]
			if not uids:
				print("Empty: %s" % (boxname,))
				continue
			
			files_uids = [("%d.eml" % int(uid), uid) for uid in uids.split(b' ')]
			filedict = dict(files_uids)
			zipname = os.path.join(folder, fin(boxname) + ".zip")
			with zipfile.ZipFile(zipname, 'a', zipfile.ZIP_DEFLATED) as z:
				has = dict(zip(z.namelist(), trues()))  # Make a dict so lookup is faster
				new = [fu for fu in files_uids if fu[0] not in has]
				was = [f for f in has if f not in filedict]
				print("Had %d of %d mails, will fetch %d from %s..." % (
					len(has), len(files_uids), len(new), boxname))
				if was:
					print("(%d stored mails nolonger in %s online)" % (len(was), boxname))
					old += len(was)
				
				had += (len(files_uids) - len(new))
				for (filename, uid) in new:
					fullname = "%s%s%s" % (zipname, os.sep, filename)
					data = do(M.uid, "FETCH", uid, '(RFC822)')[0][1]
					z.writestr(filename, data)
					print("Saved: %s (%.f kbytes)" % (fullname, len(data)/1000))
					got += 1
					kb += len(data)
	
	print("Downloaded %d emails (%.f kb) and already had %d." % (got, kb/1000, had))
	if old:
		print("(%d stored mails nolonger online)" % (old,))

if __name__ == '__main__':
	import sys
	
	if len(sys.argv) in (3, 4):
		imupp(sys.argv[1], sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else None)
	
	elif sys.argv[0].upper().endswith(".EXE"):
		server = input("IMAP mail-server: ")
		username = input("E-mail account username: ")
		imupp(server, username)
	
	else:
		print("SYNTAX: %s imapserver username [password]" % (sys.argv[0],))
