Run black on all Python scripts!
This commit is contained in:
parent
fb6cfce656
commit
cd9cbcaa28
30 changed files with 1027 additions and 704 deletions
|
@ -45,7 +45,7 @@ import notmuch
|
|||
import progressbar
|
||||
import xdg.BaseDirectory
|
||||
|
||||
MailLocation = typing.NewType('MailLocation', typing.Tuple[str, str, str])
|
||||
MailLocation = typing.NewType("MailLocation", typing.Tuple[str, str, str])
|
||||
# MessageAction = typing.Callable[[notmuch.Message], None]
|
||||
|
||||
|
||||
|
@ -106,7 +106,8 @@ class MelEngine:
|
|||
assert not self.database
|
||||
self.log.info("Indexing mails")
|
||||
notmuch_config_file = os.path.expanduser(
|
||||
"~/.config/notmuch-config") # TODO Better
|
||||
"~/.config/notmuch-config"
|
||||
) # TODO Better
|
||||
cmd = ["notmuch", "--config", notmuch_config_file, "new"]
|
||||
self.log.debug(" ".join(cmd))
|
||||
subprocess.run(cmd, check=True)
|
||||
|
@ -117,7 +118,8 @@ class MelEngine:
|
|||
"""
|
||||
assert self.config
|
||||
storage_path = os.path.realpath(
|
||||
os.path.expanduser(self.config["GENERAL"]["storage"]))
|
||||
os.path.expanduser(self.config["GENERAL"]["storage"])
|
||||
)
|
||||
folders = list()
|
||||
for account in self.accounts:
|
||||
storage_path_account = os.path.join(storage_path, account)
|
||||
|
@ -125,9 +127,9 @@ class MelEngine:
|
|||
if "cur" not in dirs or "new" not in dirs or "tmp" not in dirs:
|
||||
continue
|
||||
assert root.startswith(storage_path)
|
||||
path = root[len(storage_path):]
|
||||
path_split = path.split('/')
|
||||
if path_split[0] == '':
|
||||
path = root[len(storage_path) :]
|
||||
path_split = path.split("/")
|
||||
if path_split[0] == "":
|
||||
path_split = path_split[1:]
|
||||
folders.append(tuple(path_split))
|
||||
return folders
|
||||
|
@ -138,8 +140,11 @@ class MelEngine:
|
|||
Be sure to require only in the mode you want to avoid deadlocks.
|
||||
"""
|
||||
assert self.config
|
||||
mode = notmuch.Database.MODE.READ_WRITE if write \
|
||||
mode = (
|
||||
notmuch.Database.MODE.READ_WRITE
|
||||
if write
|
||||
else notmuch.Database.MODE.READ_ONLY
|
||||
)
|
||||
if self.database:
|
||||
# If the requested mode is the one already present,
|
||||
# or we request read when it's already write, do nothing
|
||||
|
@ -149,7 +154,8 @@ class MelEngine:
|
|||
self.close_database()
|
||||
self.log.info("Opening database in mode %s", mode)
|
||||
db_path = os.path.realpath(
|
||||
os.path.expanduser(self.config["GENERAL"]["storage"]))
|
||||
os.path.expanduser(self.config["GENERAL"]["storage"])
|
||||
)
|
||||
self.database = notmuch.Database(mode=mode, path=db_path)
|
||||
|
||||
def close_database(self) -> None:
|
||||
|
@ -171,13 +177,13 @@ class MelEngine:
|
|||
assert self.database
|
||||
base = self.database.get_path()
|
||||
assert path.startswith(base)
|
||||
path = path[len(base):]
|
||||
path_split = path.split('/')
|
||||
path = path[len(base) :]
|
||||
path_split = path.split("/")
|
||||
mailbox = path_split[1]
|
||||
assert mailbox in self.accounts
|
||||
state = path_split[-1]
|
||||
folder = tuple(path_split[2:-1])
|
||||
assert state in {'cur', 'tmp', 'new'}
|
||||
assert state in {"cur", "tmp", "new"}
|
||||
return (mailbox, folder, state)
|
||||
|
||||
@staticmethod
|
||||
|
@ -185,8 +191,11 @@ class MelEngine:
|
|||
"""
|
||||
Tells if the provided string is a valid UID.
|
||||
"""
|
||||
return isinstance(uid, str) and len(uid) == 12 \
|
||||
and bool(re.match('^[a-zA-Z0-9+/]{12}$', uid))
|
||||
return (
|
||||
isinstance(uid, str)
|
||||
and len(uid) == 12
|
||||
and bool(re.match("^[a-zA-Z0-9+/]{12}$", uid))
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def extract_email(field: str) -> str:
|
||||
|
@ -197,9 +206,9 @@ class MelEngine:
|
|||
# TODO Can be made better (extract name and email)
|
||||
# Also what happens with multiple dests?
|
||||
try:
|
||||
sta = field.index('<')
|
||||
sto = field.index('>')
|
||||
return field[sta+1:sto]
|
||||
sta = field.index("<")
|
||||
sto = field.index(">")
|
||||
return field[sta + 1 : sto]
|
||||
except ValueError:
|
||||
return field
|
||||
|
||||
|
@ -211,8 +220,9 @@ class MelEngine:
|
|||
|
||||
# Search-friendly folder name
|
||||
slug_folder_list = list()
|
||||
for fold_index, fold in [(fold_index, folder[fold_index])
|
||||
for fold_index in range(len(folder))]:
|
||||
for fold_index, fold in [
|
||||
(fold_index, folder[fold_index]) for fold_index in range(len(folder))
|
||||
]:
|
||||
if fold_index == 0 and len(folder) > 1 and fold == "INBOX":
|
||||
continue
|
||||
slug_folder_list.append(fold.upper())
|
||||
|
@ -229,14 +239,15 @@ class MelEngine:
|
|||
msg.add_tag(tag)
|
||||
elif not condition and tag in tags:
|
||||
msg.remove_tag(tag)
|
||||
expeditor = MelEngine.extract_email(msg.get_header('from'))
|
||||
|
||||
tag_if('inbox', slug_folder[0] == 'INBOX')
|
||||
tag_if('spam', slug_folder[0] in ('JUNK', 'SPAM'))
|
||||
tag_if('deleted', slug_folder[0] == 'TRASH')
|
||||
tag_if('draft', slug_folder[0] == 'DRAFTS')
|
||||
tag_if('sent', expeditor in self.aliases)
|
||||
tag_if('unprocessed', False)
|
||||
expeditor = MelEngine.extract_email(msg.get_header("from"))
|
||||
|
||||
tag_if("inbox", slug_folder[0] == "INBOX")
|
||||
tag_if("spam", slug_folder[0] in ("JUNK", "SPAM"))
|
||||
tag_if("deleted", slug_folder[0] == "TRASH")
|
||||
tag_if("draft", slug_folder[0] == "DRAFTS")
|
||||
tag_if("sent", expeditor in self.aliases)
|
||||
tag_if("unprocessed", False)
|
||||
|
||||
# UID
|
||||
uid = msg.get_header("X-TUID")
|
||||
|
@ -244,17 +255,23 @@ class MelEngine:
|
|||
# TODO Happens to sent mails but should it?
|
||||
print(f"{msg.get_filename()} has no UID!")
|
||||
return
|
||||
uidtag = 'tuid{}'.format(uid)
|
||||
uidtag = "tuid{}".format(uid)
|
||||
# Remove eventual others UID
|
||||
for tag in tags:
|
||||
if tag.startswith('tuid') and tag != uidtag:
|
||||
if tag.startswith("tuid") and tag != uidtag:
|
||||
msg.remove_tag(tag)
|
||||
msg.add_tag(uidtag)
|
||||
|
||||
def apply_msgs(self, query_str: str, action: typing.Callable,
|
||||
*args: typing.Any, show_progress: bool = False,
|
||||
write: bool = False, close_db: bool = True,
|
||||
**kwargs: typing.Any) -> int:
|
||||
def apply_msgs(
|
||||
self,
|
||||
query_str: str,
|
||||
action: typing.Callable,
|
||||
*args: typing.Any,
|
||||
show_progress: bool = False,
|
||||
write: bool = False,
|
||||
close_db: bool = True,
|
||||
**kwargs: typing.Any,
|
||||
) -> int:
|
||||
"""
|
||||
Run a function on the messages selected by the given query.
|
||||
"""
|
||||
|
@ -267,8 +284,11 @@ class MelEngine:
|
|||
elements = query.search_messages()
|
||||
nb_msgs = query.count_messages()
|
||||
|
||||
iterator = progressbar.progressbar(elements, max_value=nb_msgs) \
|
||||
if show_progress and nb_msgs else elements
|
||||
iterator = (
|
||||
progressbar.progressbar(elements, max_value=nb_msgs)
|
||||
if show_progress and nb_msgs
|
||||
else elements
|
||||
)
|
||||
|
||||
self.log.info("Executing %s", action)
|
||||
for msg in iterator:
|
||||
|
@ -296,8 +316,9 @@ class MelOutput:
|
|||
WIDTH_FIXED = 31
|
||||
WIDTH_RATIO_DEST_SUBJECT = 0.3
|
||||
|
||||
def compute_line_format(self) -> typing.Tuple[typing.Optional[int],
|
||||
typing.Optional[int]]:
|
||||
def compute_line_format(
|
||||
self,
|
||||
) -> typing.Tuple[typing.Optional[int], typing.Optional[int]]:
|
||||
"""
|
||||
Based on the terminal width, assign the width of flexible columns.
|
||||
"""
|
||||
|
@ -332,12 +353,12 @@ class MelOutput:
|
|||
"""
|
||||
now = datetime.datetime.now()
|
||||
if now - date < datetime.timedelta(days=1):
|
||||
return date.strftime('%H:%M:%S')
|
||||
return date.strftime("%H:%M:%S")
|
||||
if now - date < datetime.timedelta(days=28):
|
||||
return date.strftime('%d %H:%M')
|
||||
return date.strftime("%d %H:%M")
|
||||
if now - date < datetime.timedelta(days=365):
|
||||
return date.strftime('%m-%d %H')
|
||||
return date.strftime('%y-%m-%d')
|
||||
return date.strftime("%m-%d %H")
|
||||
return date.strftime("%y-%m-%d")
|
||||
|
||||
@staticmethod
|
||||
def clip_text(size: typing.Optional[int], text: str) -> str:
|
||||
|
@ -352,28 +373,28 @@ class MelOutput:
|
|||
if length == size:
|
||||
return text
|
||||
if length > size:
|
||||
return text[:size-1] + '…'
|
||||
return text + ' ' * (size - length)
|
||||
return text[: size - 1] + "…"
|
||||
return text + " " * (size - length)
|
||||
|
||||
@staticmethod
|
||||
def chunks(iterable: str, chunk_size: int) -> typing.Iterable[str]:
|
||||
"""Yield successive chunk_size-sized chunks from iterable."""
|
||||
# From https://stackoverflow.com/a/312464
|
||||
for i in range(0, len(iterable), chunk_size):
|
||||
yield iterable[i:i + chunk_size]
|
||||
yield iterable[i : i + chunk_size]
|
||||
|
||||
@staticmethod
|
||||
def sizeof_fmt(num: int, suffix: str = 'B') -> str:
|
||||
def sizeof_fmt(num: int, suffix: str = "B") -> str:
|
||||
"""
|
||||
Print the given size in a human-readable format.
|
||||
"""
|
||||
remainder = float(num)
|
||||
# From https://stackoverflow.com/a/1094933
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
|
||||
if abs(remainder) < 1024.0:
|
||||
return "%3.1f %s%s" % (remainder, unit, suffix)
|
||||
remainder /= 1024.0
|
||||
return "%.1f %s%s" % (remainder, 'Yi', suffix)
|
||||
return "%.1f %s%s" % (remainder, "Yi", suffix)
|
||||
|
||||
def get_mailbox_color(self, mailbox: str) -> str:
|
||||
"""
|
||||
|
@ -381,7 +402,7 @@ class MelOutput:
|
|||
string with ASCII escape codes.
|
||||
"""
|
||||
if not self.is_tty:
|
||||
return ''
|
||||
return ""
|
||||
if mailbox not in self.mailbox_colors:
|
||||
# RGB colors (not supported everywhere)
|
||||
# color_str = self.config[mailbox]["color"]
|
||||
|
@ -406,21 +427,24 @@ class MelOutput:
|
|||
line = ""
|
||||
tags = set(msg.get_tags())
|
||||
mailbox, _, _ = self.engine.get_location(msg)
|
||||
if 'unread' in tags or 'flagged' in tags:
|
||||
if "unread" in tags or "flagged" in tags:
|
||||
line += colorama.Style.BRIGHT
|
||||
# if 'flagged' in tags:
|
||||
# line += colorama.Style.BRIGHT
|
||||
# if 'unread' not in tags:
|
||||
# line += colorama.Style.DIM
|
||||
line += colorama.Back.LIGHTBLACK_EX if self.light_background \
|
||||
line += (
|
||||
colorama.Back.LIGHTBLACK_EX
|
||||
if self.light_background
|
||||
else colorama.Back.BLACK
|
||||
)
|
||||
self.light_background = not self.light_background
|
||||
line += self.get_mailbox_color(mailbox)
|
||||
|
||||
# UID
|
||||
uid = None
|
||||
for tag in tags:
|
||||
if tag.startswith('tuid'):
|
||||
if tag.startswith("tuid"):
|
||||
uid = tag[4:]
|
||||
assert uid, f"No UID for message: {msg}."
|
||||
assert MelEngine.is_uid(uid), f"{uid} {type(uid)} is not a valid UID."
|
||||
|
@ -434,8 +458,9 @@ class MelOutput:
|
|||
# Icons
|
||||
line += sep + colorama.Fore.RED
|
||||
|
||||
def tags2col1(tag1: str, tag2: str,
|
||||
characters: typing.Tuple[str, str, str, str]) -> None:
|
||||
def tags2col1(
|
||||
tag1: str, tag2: str, characters: typing.Tuple[str, str, str, str]
|
||||
) -> None:
|
||||
"""
|
||||
Show the presence/absence of two tags with one character.
|
||||
"""
|
||||
|
@ -452,14 +477,14 @@ class MelOutput:
|
|||
else:
|
||||
line += none
|
||||
|
||||
tags2col1('spam', 'draft', ('?', 'S', 'D', ' '))
|
||||
tags2col1('attachment', 'encrypted', ('E', 'A', 'E', ' '))
|
||||
tags2col1('unread', 'flagged', ('!', 'U', 'F', ' '))
|
||||
tags2col1('sent', 'replied', ('?', '↑', '↪', ' '))
|
||||
tags2col1("spam", "draft", ("?", "S", "D", " "))
|
||||
tags2col1("attachment", "encrypted", ("E", "A", "E", " "))
|
||||
tags2col1("unread", "flagged", ("!", "U", "F", " "))
|
||||
tags2col1("sent", "replied", ("?", "↑", "↪", " "))
|
||||
|
||||
# Opposed
|
||||
line += sep + colorama.Fore.BLUE
|
||||
if 'sent' in tags:
|
||||
if "sent" in tags:
|
||||
dest = msg.get_header("to")
|
||||
else:
|
||||
dest = msg.get_header("from")
|
||||
|
@ -483,11 +508,10 @@ class MelOutput:
|
|||
expd = msg.get_header("from")
|
||||
account, _, _ = self.engine.get_location(msg)
|
||||
|
||||
summary = '{} (<i>{}</i>)'.format(html.escape(expd), account)
|
||||
summary = "{} (<i>{}</i>)".format(html.escape(expd), account)
|
||||
body = html.escape(subject)
|
||||
cmd = ["notify-send", "-u", "low", "-i",
|
||||
"mail-message-new", summary, body]
|
||||
print(' '.join(cmd))
|
||||
cmd = ["notify-send", "-u", "low", "-i", "mail-message-new", summary, body]
|
||||
print(" ".join(cmd))
|
||||
subprocess.run(cmd, check=False)
|
||||
|
||||
def notify_all(self) -> None:
|
||||
|
@ -497,12 +521,26 @@ class MelOutput:
|
|||
since it should be marked as processed right after.
|
||||
"""
|
||||
nb_msgs = self.engine.apply_msgs(
|
||||
'tag:unread and tag:unprocessed', self.notify_msg)
|
||||
"tag:unread and tag:unprocessed", self.notify_msg
|
||||
)
|
||||
if nb_msgs > 0:
|
||||
self.log.info(
|
||||
"Playing notification sound (%d new message(s))", nb_msgs)
|
||||
cmd = ["play", "-n", "synth", "sine", "E4", "sine", "A5",
|
||||
"remix", "1-2", "fade", "0.5", "1.2", "0.5", "2"]
|
||||
self.log.info("Playing notification sound (%d new message(s))", nb_msgs)
|
||||
cmd = [
|
||||
"play",
|
||||
"-n",
|
||||
"synth",
|
||||
"sine",
|
||||
"E4",
|
||||
"sine",
|
||||
"A5",
|
||||
"remix",
|
||||
"1-2",
|
||||
"fade",
|
||||
"0.5",
|
||||
"1.2",
|
||||
"0.5",
|
||||
"2",
|
||||
]
|
||||
subprocess.run(cmd, check=False)
|
||||
|
||||
@staticmethod
|
||||
|
@ -510,46 +548,62 @@ class MelOutput:
|
|||
"""
|
||||
Return split header values in a contiguous string.
|
||||
"""
|
||||
return val.replace('\n', '').replace('\t', '').strip()
|
||||
return val.replace("\n", "").replace("\t", "").strip()
|
||||
|
||||
PART_MULTI_FORMAT = colorama.Fore.BLUE + \
|
||||
'{count} {indent}+ {typ}' + colorama.Style.RESET_ALL
|
||||
PART_LEAF_FORMAT = colorama.Fore.BLUE + \
|
||||
'{count} {indent}→ {desc} ({typ}; {size})' + \
|
||||
colorama.Style.RESET_ALL
|
||||
PART_MULTI_FORMAT = (
|
||||
colorama.Fore.BLUE + "{count} {indent}+ {typ}" + colorama.Style.RESET_ALL
|
||||
)
|
||||
PART_LEAF_FORMAT = (
|
||||
colorama.Fore.BLUE
|
||||
+ "{count} {indent}→ {desc} ({typ}; {size})"
|
||||
+ colorama.Style.RESET_ALL
|
||||
)
|
||||
|
||||
def show_parts_tree(self, part: email.message.Message,
|
||||
depth: int = 0, count: int = 1) -> int:
|
||||
def show_parts_tree(
|
||||
self, part: email.message.Message, depth: int = 0, count: int = 1
|
||||
) -> int:
|
||||
"""
|
||||
Show a tree of the parts contained in a message.
|
||||
Return the number of parts of the mesage.
|
||||
"""
|
||||
indent = depth * '\t'
|
||||
indent = depth * "\t"
|
||||
typ = part.get_content_type()
|
||||
|
||||
if part.is_multipart():
|
||||
print(MelOutput.PART_MULTI_FORMAT.format(
|
||||
count=count, indent=indent, typ=typ))
|
||||
print(
|
||||
MelOutput.PART_MULTI_FORMAT.format(count=count, indent=indent, typ=typ)
|
||||
)
|
||||
payl = part.get_payload()
|
||||
assert isinstance(payl, list)
|
||||
size = 1
|
||||
for obj in payl:
|
||||
size += self.show_parts_tree(obj, depth=depth+1,
|
||||
count=count+size)
|
||||
size += self.show_parts_tree(obj, depth=depth + 1, count=count + size)
|
||||
return size
|
||||
|
||||
payl = part.get_payload(decode=True)
|
||||
assert isinstance(payl, bytes)
|
||||
size = len(payl)
|
||||
desc = part.get('Content-Description', '<no description>')
|
||||
print(MelOutput.PART_LEAF_FORMAT.format(
|
||||
count=count, indent=indent, typ=typ, desc=desc,
|
||||
size=MelOutput.sizeof_fmt(size)))
|
||||
desc = part.get("Content-Description", "<no description>")
|
||||
print(
|
||||
MelOutput.PART_LEAF_FORMAT.format(
|
||||
count=count,
|
||||
indent=indent,
|
||||
typ=typ,
|
||||
desc=desc,
|
||||
size=MelOutput.sizeof_fmt(size),
|
||||
)
|
||||
)
|
||||
return 1
|
||||
|
||||
INTERESTING_HEADERS = ["Date", "From", "Subject", "To", "Cc", "Message-Id"]
|
||||
HEADER_FORMAT = colorama.Fore.BLUE + colorama.Style.BRIGHT + \
|
||||
'{}:' + colorama.Style.NORMAL + ' {}' + colorama.Style.RESET_ALL
|
||||
HEADER_FORMAT = (
|
||||
colorama.Fore.BLUE
|
||||
+ colorama.Style.BRIGHT
|
||||
+ "{}:"
|
||||
+ colorama.Style.NORMAL
|
||||
+ " {}"
|
||||
+ colorama.Style.RESET_ALL
|
||||
)
|
||||
|
||||
def read_msg(self, msg: notmuch.Message) -> None:
|
||||
"""
|
||||
|
@ -558,7 +612,7 @@ class MelOutput:
|
|||
# Parse
|
||||
filename = msg.get_filename()
|
||||
parser = email.parser.BytesParser()
|
||||
with open(filename, 'rb') as filedesc:
|
||||
with open(filename, "rb") as filedesc:
|
||||
mail = parser.parse(filedesc)
|
||||
|
||||
# Defects
|
||||
|
@ -591,12 +645,13 @@ class MelOutput:
|
|||
print(payl.decode())
|
||||
else:
|
||||
# TODO Use nametemplate from mailcap
|
||||
temp_file = '/tmp/melcap.html' # TODO Real temporary file
|
||||
temp_file = "/tmp/melcap.html" # TODO Real temporary file
|
||||
# TODO FIFO if possible
|
||||
with open(temp_file, 'wb') as temp_filedesc:
|
||||
with open(temp_file, "wb") as temp_filedesc:
|
||||
temp_filedesc.write(payl)
|
||||
command, _ = mailcap.findmatch(
|
||||
self.caps, part.get_content_type(), key='view', filename=temp_file)
|
||||
self.caps, part.get_content_type(), key="view", filename=temp_file
|
||||
)
|
||||
if command:
|
||||
os.system(command)
|
||||
|
||||
|
@ -611,21 +666,26 @@ class MelOutput:
|
|||
line += arb[0].replace("'", "\\'")
|
||||
line += colorama.Fore.LIGHTBLACK_EX
|
||||
for inter in arb[1:-1]:
|
||||
line += '/' + inter.replace("'", "\\'")
|
||||
line += '/' + colorama.Fore.WHITE + arb[-1].replace("'", "\\'")
|
||||
line += "/" + inter.replace("'", "\\'")
|
||||
line += "/" + colorama.Fore.WHITE + arb[-1].replace("'", "\\'")
|
||||
line += colorama.Fore.LIGHTBLACK_EX + "'"
|
||||
line += colorama.Style.RESET_ALL
|
||||
print(line)
|
||||
|
||||
|
||||
class MelCLI():
|
||||
class MelCLI:
|
||||
"""
|
||||
Handles the user input and run asked operations.
|
||||
"""
|
||||
|
||||
VERBOSITY_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "FATAL"]
|
||||
|
||||
def apply_msgs_input(self, argmessages: typing.List[str],
|
||||
action: typing.Callable, write: bool = False) -> None:
|
||||
def apply_msgs_input(
|
||||
self,
|
||||
argmessages: typing.List[str],
|
||||
action: typing.Callable,
|
||||
write: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Run a function on the message given by the user.
|
||||
"""
|
||||
|
@ -633,7 +693,7 @@ class MelCLI():
|
|||
if not argmessages:
|
||||
from_stdin = not sys.stdin.isatty()
|
||||
if argmessages:
|
||||
from_stdin = len(argmessages) == 1 and argmessages == '-'
|
||||
from_stdin = len(argmessages) == 1 and argmessages == "-"
|
||||
|
||||
messages = list()
|
||||
if from_stdin:
|
||||
|
@ -646,9 +706,11 @@ class MelCLI():
|
|||
else:
|
||||
for uids in argmessages:
|
||||
if len(uids) > 12:
|
||||
self.log.warning("Might have forgotten some spaces "
|
||||
"between the UIDs. Don't worry, I'll "
|
||||
"split them for you")
|
||||
self.log.warning(
|
||||
"Might have forgotten some spaces "
|
||||
"between the UIDs. Don't worry, I'll "
|
||||
"split them for you"
|
||||
)
|
||||
for uid in MelOutput.chunks(uids, 12):
|
||||
if not MelEngine.is_uid(uid):
|
||||
self.log.error("Not an UID: %s", uid)
|
||||
|
@ -656,48 +718,52 @@ class MelCLI():
|
|||
messages.append(uid)
|
||||
|
||||
for message in messages:
|
||||
query_str = f'tag:tuid{message}'
|
||||
nb_msgs = self.engine.apply_msgs(query_str, action,
|
||||
write=write, close_db=False)
|
||||
query_str = f"tag:tuid{message}"
|
||||
nb_msgs = self.engine.apply_msgs(
|
||||
query_str, action, write=write, close_db=False
|
||||
)
|
||||
if nb_msgs < 1:
|
||||
self.log.error(
|
||||
"Couldn't execute function for message %s", message)
|
||||
self.log.error("Couldn't execute function for message %s", message)
|
||||
self.engine.close_database()
|
||||
|
||||
def operation_default(self) -> None:
|
||||
"""
|
||||
Default operation: list all message in the inbox
|
||||
"""
|
||||
self.engine.apply_msgs('tag:inbox', self.output.print_msg)
|
||||
self.engine.apply_msgs("tag:inbox", self.output.print_msg)
|
||||
|
||||
def operation_inbox(self) -> None:
|
||||
"""
|
||||
Inbox operation: list all message in the inbox,
|
||||
possibly only the unread ones.
|
||||
"""
|
||||
query_str = 'tag:unread' if self.args.only_unread else 'tag:inbox'
|
||||
query_str = "tag:unread" if self.args.only_unread else "tag:inbox"
|
||||
self.engine.apply_msgs(query_str, self.output.print_msg)
|
||||
|
||||
def operation_flag(self) -> None:
|
||||
"""
|
||||
Flag operation: Flag user selected messages.
|
||||
"""
|
||||
|
||||
def flag_msg(msg: notmuch.Message) -> None:
|
||||
"""
|
||||
Flag given message.
|
||||
"""
|
||||
msg.add_tag('flagged')
|
||||
msg.add_tag("flagged")
|
||||
|
||||
self.apply_msgs_input(self.args.message, flag_msg, write=True)
|
||||
|
||||
def operation_unflag(self) -> None:
|
||||
"""
|
||||
Unflag operation: Flag user selected messages.
|
||||
"""
|
||||
|
||||
def unflag_msg(msg: notmuch.Message) -> None:
|
||||
"""
|
||||
Unflag given message.
|
||||
"""
|
||||
msg.remove_tag('flagged')
|
||||
msg.remove_tag("flagged")
|
||||
|
||||
self.apply_msgs_input(self.args.message, unflag_msg, write=True)
|
||||
|
||||
def operation_read(self) -> None:
|
||||
|
@ -712,8 +778,7 @@ class MelCLI():
|
|||
"""
|
||||
# Fetch mails
|
||||
self.log.info("Fetching mails")
|
||||
mbsync_config_file = os.path.expanduser(
|
||||
"~/.config/mbsyncrc") # TODO Better
|
||||
mbsync_config_file = os.path.expanduser("~/.config/mbsyncrc") # TODO Better
|
||||
cmd = ["mbsync", "--config", mbsync_config_file, "--all"]
|
||||
subprocess.run(cmd, check=False)
|
||||
|
||||
|
@ -724,8 +789,9 @@ class MelCLI():
|
|||
self.output.notify_all()
|
||||
|
||||
# Tag new mails
|
||||
self.engine.apply_msgs('tag:unprocessed', self.engine.retag_msg,
|
||||
show_progress=True, write=True)
|
||||
self.engine.apply_msgs(
|
||||
"tag:unprocessed", self.engine.retag_msg, show_progress=True, write=True
|
||||
)
|
||||
|
||||
def operation_list(self) -> None:
|
||||
"""
|
||||
|
@ -744,14 +810,15 @@ class MelCLI():
|
|||
Retag operation: Manually retag all the mails in the database.
|
||||
Mostly debug I suppose.
|
||||
"""
|
||||
self.engine.apply_msgs('*', self.engine.retag_msg,
|
||||
show_progress=True, write=True)
|
||||
self.engine.apply_msgs(
|
||||
"*", self.engine.retag_msg, show_progress=True, write=True
|
||||
)
|
||||
|
||||
def operation_all(self) -> None:
|
||||
"""
|
||||
All operation: list every single message.
|
||||
"""
|
||||
self.engine.apply_msgs('*', self.output.print_msg)
|
||||
self.engine.apply_msgs("*", self.output.print_msg)
|
||||
|
||||
def add_subparsers(self) -> None:
|
||||
"""
|
||||
|
@ -766,29 +833,30 @@ class MelCLI():
|
|||
|
||||
# inbox (default)
|
||||
parser_inbox = subparsers.add_parser(
|
||||
"inbox", help="Show unread, unsorted and flagged messages")
|
||||
parser_inbox.add_argument('-u', '--only-unread', action='store_true',
|
||||
help="Show unread messages only")
|
||||
"inbox", help="Show unread, unsorted and flagged messages"
|
||||
)
|
||||
parser_inbox.add_argument(
|
||||
"-u", "--only-unread", action="store_true", help="Show unread messages only"
|
||||
)
|
||||
# TODO Make this more relevant
|
||||
parser_inbox.set_defaults(operation=self.operation_inbox)
|
||||
|
||||
# list folder [--recurse]
|
||||
# List actions
|
||||
parser_list = subparsers.add_parser(
|
||||
"list", help="List all folders")
|
||||
parser_list = subparsers.add_parser("list", help="List all folders")
|
||||
# parser_list.add_argument('message', nargs='*', help="Messages")
|
||||
parser_list.set_defaults(operation=self.operation_list)
|
||||
|
||||
# flag msg...
|
||||
parser_flag = subparsers.add_parser(
|
||||
"flag", help="Mark messages as flagged")
|
||||
parser_flag.add_argument('message', nargs='*', help="Messages")
|
||||
parser_flag = subparsers.add_parser("flag", help="Mark messages as flagged")
|
||||
parser_flag.add_argument("message", nargs="*", help="Messages")
|
||||
parser_flag.set_defaults(operation=self.operation_flag)
|
||||
|
||||
# unflag msg...
|
||||
parser_unflag = subparsers.add_parser(
|
||||
"unflag", help="Mark messages as not-flagged")
|
||||
parser_unflag.add_argument('message', nargs='*', help="Messages")
|
||||
"unflag", help="Mark messages as not-flagged"
|
||||
)
|
||||
parser_unflag.add_argument("message", nargs="*", help="Messages")
|
||||
parser_unflag.set_defaults(operation=self.operation_unflag)
|
||||
|
||||
# delete msg...
|
||||
|
@ -799,7 +867,7 @@ class MelCLI():
|
|||
# read msg [--html] [--plain] [--browser]
|
||||
|
||||
parser_read = subparsers.add_parser("read", help="Read message")
|
||||
parser_read.add_argument('message', nargs=1, help="Messages")
|
||||
parser_read.add_argument("message", nargs=1, help="Messages")
|
||||
parser_read.set_defaults(operation=self.operation_read)
|
||||
|
||||
# attach msg [id] [--save] (list if no id, xdg-open else)
|
||||
|
@ -817,20 +885,23 @@ class MelCLI():
|
|||
# fetch (mbsync, notmuch new, retag, notify; called by greater gods)
|
||||
|
||||
parser_fetch = subparsers.add_parser(
|
||||
"fetch", help="Fetch mail, tag them, and run notifications")
|
||||
"fetch", help="Fetch mail, tag them, and run notifications"
|
||||
)
|
||||
parser_fetch.set_defaults(operation=self.operation_fetch)
|
||||
|
||||
# Debug
|
||||
|
||||
# debug (various)
|
||||
parser_debug = subparsers.add_parser(
|
||||
"debug", help="Who know what this holds...")
|
||||
parser_debug.set_defaults(verbosity='DEBUG')
|
||||
"debug", help="Who know what this holds..."
|
||||
)
|
||||
parser_debug.set_defaults(verbosity="DEBUG")
|
||||
parser_debug.set_defaults(operation=self.operation_debug)
|
||||
|
||||
# retag (all or unprocessed)
|
||||
parser_retag = subparsers.add_parser(
|
||||
"retag", help="Retag all mails (when you changed configuration)")
|
||||
"retag", help="Retag all mails (when you changed configuration)"
|
||||
)
|
||||
parser_retag.set_defaults(operation=self.operation_retag)
|
||||
|
||||
# all
|
||||
|
@ -842,15 +913,21 @@ class MelCLI():
|
|||
Create the main parser that will handle the user arguments.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="Meh mail client")
|
||||
parser.add_argument('-v', '--verbosity',
|
||||
choices=MelCLI.VERBOSITY_LEVELS, default='WARNING',
|
||||
help="Verbosity of self.log messages")
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbosity",
|
||||
choices=MelCLI.VERBOSITY_LEVELS,
|
||||
default="WARNING",
|
||||
help="Verbosity of self.log messages",
|
||||
)
|
||||
# parser.add_argument('-n', '--dry-run', action='store_true',
|
||||
# help="Don't do anything") # DEBUG
|
||||
default_config_file = os.path.join(
|
||||
xdg.BaseDirectory.xdg_config_home, 'mel', 'accounts.conf')
|
||||
parser.add_argument('-c', '--config', default=default_config_file,
|
||||
help="Accounts config file")
|
||||
xdg.BaseDirectory.xdg_config_home, "mel", "accounts.conf"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--config", default=default_config_file, help="Accounts config file"
|
||||
)
|
||||
return parser
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
@ -860,8 +937,9 @@ class MelCLI():
|
|||
self.add_subparsers()
|
||||
|
||||
self.args = self.parser.parse_args()
|
||||
coloredlogs.install(level=self.args.verbosity,
|
||||
fmt='%(levelname)s %(name)s %(message)s')
|
||||
coloredlogs.install(
|
||||
level=self.args.verbosity, fmt="%(levelname)s %(name)s %(message)s"
|
||||
)
|
||||
|
||||
self.engine = MelEngine(self.args.config)
|
||||
self.output = MelOutput(self.engine)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue