From 18a7278422c49ecbb717453b2097833d919ba1be Mon Sep 17 00:00:00 2001 From: Geoffrey Frogeye Date: Mon, 13 Aug 2018 17:59:40 +0200 Subject: [PATCH] mel base --- scripts/mel | 139 +++++++++++++++++++++++++++++++++++++++--------- scripts/melConf | 95 +++++++++++++++++++++++++-------- 2 files changed, 188 insertions(+), 46 deletions(-) diff --git a/scripts/mel b/scripts/mel index 2eecbe4..4922bd1 100755 --- a/scripts/mel +++ b/scripts/mel @@ -4,6 +4,9 @@ Meh mail client """ +# TODO Features +# TODO (only then) Refactor + import notmuch import logging import coloredlogs @@ -12,14 +15,40 @@ import datetime import os import progressbar import time +import argparse +import configparser +import base64 +import shutil colorama.init() coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s') log = logging.getLogger() +log.debug("Loading config") + +# TODO XDG +configPath = os.path.join(os.path.expanduser('~'), '.config', 'mel', 'accounts.conf') + +config = configparser.ConfigParser() +config.read(configPath) + +# Reading config a bit +accounts = dict() +mails = set() +for name in config.sections(): + if not name.islower(): + continue + section = config[name] + mails.add(section["from"]) + if "alternatives" in section: + for alt in section["alternatives"].split(";"): + mails.add(alt) + accounts[name] = section + log.debug("Loading database") -db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) +dbPath = os.path.realpath(os.path.expanduser(config["GENERAL"]["storage"])) +db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE, path=dbPath) # TODO Open read-only when needed log.debug("Database loaded") @@ -30,7 +59,10 @@ def get_location(msg): base = db.get_path() assert path.startswith(base) path = path[len(base):] - _, mailbox, folder, state = path.split('/') + pathSplit = path.split('/') + mailbox = pathSplit[1] + state = pathSplit[-1] + folder = tuple(pathSplit[2:-1]) assert state in {'cur', 'tmp', 'new'} return (mailbox, folder, state) @@ -38,10 +70,7 @@ MAILBOX_COLORS = dict() def get_mailbox_color(mailbox): if mailbox not in MAILBOX_COLORS: - colorfile = os.path.join(db.get_path(), mailbox, 'color') - assert os.path.isfile(colorfile) - with open(colorfile, 'r') as f: - colorStr = f.read() + colorStr = config[mailbox]["color"] colorStr = colorStr[1:] if colorStr[0] == '#' else colorStr R = int(colorStr[0:2], 16) G = int(colorStr[2:4], 16) @@ -57,14 +86,50 @@ def format_date(date): else: return date.strftime('%d/%m/%y') +def threadIdToB64(tid): + assert len(tid) == 16 + tidInt = int(tid, 16) + tidBytes = tidInt.to_bytes(8, 'big') + tidB64 = base64.b64encode(tidBytes) + assert len(tidB64) == 12 + return tidB64.decode() + +WIDTH_FIXED = 30 +WIDTH_RATIO_DEST_SUBJECT = 0.3 +destWidth = None +subjectWidth = None +def compute_line_format(): + columns, rows = shutil.get_terminal_size((80, 20)) + remain = columns - WIDTH_FIXED - 1 + global destWidth, subjectWidth + destWidth = int(remain * WIDTH_RATIO_DEST_SUBJECT) + subjectWidth = remain - destWidth + +def clip_text(size, text): + l = len(text) + if l == size: + return text + elif l > size: + return text[:size-1] + '…' + elif l < size: + return text + " " * (size - l) + def print_msg(msg): + if not destWidth: + compute_line_format() + line = "" tags = set(msg.get_tags()) mailbox, folder, state = get_location(msg) line += get_mailbox_color(mailbox) + # ID + line += threadIdToB64(msg.get_thread_id()) + # line += str(int(msg.get_thread_id(), 16)) + # Date + line += " " date = datetime.datetime.fromtimestamp(msg.get_date()) line += format_date(date) @@ -83,16 +148,23 @@ def print_msg(msg): else: line += none - tags2col1('spam', 'draft', '??', '💥', '📝', ' ') - tags2col1('attachment', 'encrypted', '🔐', '📎', '🔑', ' ') - tags2col1('unread', 'flagged', '🏁', '🏳 ', '🏴', ' ') + tags2col1('spam', 'draft', '?', 'S', 'D', ' ') + tags2col1('attachment', 'encrypted', 'E', 'A', 'E', ' ') + tags2col1('unread', 'flagged', '!', 'U', 'F', ' ') tags2col1('sent', 'replied', '?', '↑', '↪', ' ') - # TODO To: / From: + if 'sent' in tags: + dest = msg.get_header("to") + else: + dest = msg.get_header("from") + line += " " + line += clip_text(destWidth, dest) # Subject line += " " - line += msg.get_header("subject") + subject = msg.get_header("subject") + line += clip_text(subjectWidth, subject) + line += colorama.Style.RESET_ALL print(line) @@ -102,26 +174,33 @@ def retag_msg(msg): mailbox, folder, state = get_location(msg) # Search-friendly folder name - if folder.startswith('INBOX.'): - folder = folder[6:] - folder = folder.upper() + slugFolderList = list() + for f, fold in [(f, folder[f]) for f in range(len(folder))]: + if f == 0 and len(folder) > 1 and fold == "INBOX": + continue + slugFolderList.append(fold.upper()) + slugFolder = tuple(slugFolderList) - msg.remove_all_tags() - if folder.startswith('JUNK') or folder.startswith('SPAM'): - msg.add_tag('spam') - if folder.startswith('DRAFT'): - msg.add_tag('draft') - if folder.startswith('INBOX'): - msg.add_tag('inbox') + tags = set(msg.get_tags()) + def tag_if(tag, condition): + if condition and tag not in tags: + msg.add_tag(tag) + elif not condition and tag in tags: + msg.remove_tag(tag) + expeditor = extract_email(msg.get_header('from')) - # TODO 'sent' tag + tag_if('inbox', slugFolder[0] == 'INBOX') + tag_if('spam', slugFolder[0] == 'JUNK' or slugFolder[0] == 'SPAM') + tag_if('deleted', slugFolder[0] == 'TRASH') + tag_if('draft', slugFolder[0] == 'DRAFTS') + tag_if('sent', expeditor in mails) # Save msg.thaw() msg.tags_to_maildir_flags() -def applyMsgs(queryStr, function, useProgressbar=False): +def applyMsgs(queryStr, function, *args, useProgressbar=False, **kwargs): query = notmuch.Query(db, queryStr) query.set_sort(notmuch.Query.SORT.OLDEST_FIRST) msgs = query.search_messages() @@ -131,11 +210,23 @@ def applyMsgs(queryStr, function, useProgressbar=False): else: iterator = msgs for msg in iterator: - function(msg) + function(msg, *args, **kwargs) +def extract_email(field): + try: + sta = field.index('<') + sto = field.index('>') + return field[sta+1:sto] + except ValueError: + return field + +# applyMsgs('*', print_msg) applyMsgs('tag:inbox', print_msg) # applyMsgs('tag:spam', print_msg) # applyMsgs('tag:unread', print_msg) +# applyMsgs('tag:unprocessed', print_msg) # applyMsgs('from:geoffrey@frogeye.fr', print_msg) +# applyMsgs('tag:unprocessed', retag_msg, useProgressbar=True) # applyMsgs('*', retag_msg, useProgressbar=True) + diff --git a/scripts/melConf b/scripts/melConf index 3e850da..a0c2829 100755 --- a/scripts/melConf +++ b/scripts/melConf @@ -9,16 +9,18 @@ import os import sys # TODO Find config file from XDG -# TODO Alias adresses # TODO Signature file # TODO Write ~/.mail/[mailbox]/color file if required by sth? -# Certificate file +# TODO Fix IMAPS with mbsync -configPath = os.path.join(os.path.expanduser('~'), '.config', 'mel.conf') +configPath = os.path.join(os.path.expanduser('~'), '.config', 'mel', 'accounts.conf') config = configparser.ConfigParser() config.read(configPath) +storageFull = os.path.realpath(os.path.expanduser(config["GENERAL"]["storage"])) +config["GENERAL"]["storage"] = storageFull + SERVER_DEFAULTS = { "imap": {"port": 143, "starttls": True}, "smtp": {"port": 587, "starttls": True}, @@ -27,6 +29,7 @@ SERVER_ITEMS = {"host", "port", "user", "pass", "starttls"} # Reading sections accounts = dict() +mails = set() for name in config.sections(): if not name.islower(): @@ -56,13 +59,23 @@ for name in config.sections(): continue data[key] = section[key] - data["name"] = name - data["storage"] = os.path.join(config['GLOBAL']['storage'], name) + mails.add(section["from"]) + if "alternatives" in section: + for alt in section["alternatives"].split(";"): + mails.add(alt) + + data["account"] = name + data["storage"] = os.path.join(config['GENERAL']['storage'], name) data["storageInbox"] = os.path.join(data["storage"], "INBOX") - storageFull = os.path.expanduser(data["storage"]) - os.makedirs(storageFull, exist_ok=True) accounts[name] = data +general = dict() +section = config["GENERAL"] +for key in section.keys(): + general[key] = section[key] +general["main"] = accounts[general["main"]] + + # OfflineIMAP OFFLINEIMAP_BEGIN = """[general] @@ -82,19 +95,19 @@ footer = "\\n" """ -OFFLINEIMAP_ACCOUNT = """[Account {name}] -localrepository = {name}-local -remoterepository = {name}-remote +OFFLINEIMAP_ACCOUNT = """[Account {account}] +localrepository = {account}-local +remoterepository = {account}-remote autorefresh = 0.5 quick = 10 utf8foldernames = yes postsynchook = ~/.mutt/postsync -[Repository {name}-local] +[Repository {account}-local] type = Maildir localfolders = {storage} -[Repository {name}-remote] +[Repository {account}-remote] type = IMAP {secconf} keepalive = 60 @@ -116,23 +129,24 @@ for name, account in accounts.items(): # TODO Write # mbsync -MBSYNC_ACCOUNT = """IMAPAccount {name} +MBSYNC_ACCOUNT = """IMAPAccount {account} Host {imaphost} +Port {imapport} User {imapuser} -Pass "{imappass}" +Pass "{imappassEscaped}" {secconf} -IMAPStore {name}-remote -Account {name} +IMAPStore {account}-remote +Account {account} -MaildirStore {name}-local +MaildirStore {account}-local Subfolders Verbatim Path {storage}/ Inbox {storageInbox}/ -Channel {name} -Master :{name}-remote: -Slave :{name}-local: +Channel {account} +Master :{account}-remote: +Slave :{account}-local: Patterns * Create Both SyncState * @@ -145,7 +159,10 @@ for name, account in accounts.items(): secconf = "SSLType STARTTLS" else: secconf = "SSLType IMAPS" - mbsyncStr += MBSYNC_ACCOUNT.format(**account, secconf=secconf) + if "certificate" in account: + secconf += "\nCertificateFile {certificate}".format(**account) + imappassEscaped = account["imappass"].replace("\\", "\\\\") + mbsyncStr += MBSYNC_ACCOUNT.format(**account, secconf=secconf, imappassEscaped=imappassEscaped) msbsyncFilepath = os.path.join(os.path.expanduser('~'), '.mbsyncrc') with open(msbsyncFilepath, 'w') as f: f.write(mbsyncStr) @@ -158,7 +175,7 @@ tls_trust_file /etc/ssl/certs/ca-certificates.crt """ -MSMTP_ACCOUNT = """account {name} +MSMTP_ACCOUNT = """account {account} from {from} user {smtpuser} password {smtppass} @@ -174,3 +191,37 @@ for name, account in accounts.items(): msbsyncFilepath = os.path.join(os.path.expanduser('~'), '.msmtprc') with open(msbsyncFilepath, 'w') as f: f.write(msmtpStr) + + +# notmuch +NOTMUCH_BEGIN = """[database] +path={storage} + +[user] +name={main[name]} +primary_email={main[from]} +other_email={other_email} + +[new] +tags=unprocessed; +ignore= + +[search] +exclude_tags=deleted;spam; + +[maildir] +synchronize_flags=true + +[crypto] +gpg_path=gpg + +""" + +other_email = mails.copy() +other_email.remove(general["main"]["from"]) +other_email = ";".join(other_email) +notmuchStr = NOTMUCH_BEGIN.format(**general, other_email=other_email) +msbsyncFilepath = os.path.join(os.path.expanduser('~'), '.notmuchrc') +with open(msbsyncFilepath, 'w') as f: + f.write(notmuchStr) +