mel base
This commit is contained in:
parent
86bc146125
commit
18a7278422
139
scripts/mel
139
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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue