mel base
This commit is contained in:
parent
86bc146125
commit
18a7278422
139
scripts/mel
139
scripts/mel
|
@ -4,6 +4,9 @@
|
||||||
Meh mail client
|
Meh mail client
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# TODO Features
|
||||||
|
# TODO (only then) Refactor
|
||||||
|
|
||||||
import notmuch
|
import notmuch
|
||||||
import logging
|
import logging
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
|
@ -12,14 +15,40 @@ import datetime
|
||||||
import os
|
import os
|
||||||
import progressbar
|
import progressbar
|
||||||
import time
|
import time
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
|
import base64
|
||||||
|
import shutil
|
||||||
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
|
||||||
log = logging.getLogger()
|
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")
|
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
|
# TODO Open read-only when needed
|
||||||
|
|
||||||
log.debug("Database loaded")
|
log.debug("Database loaded")
|
||||||
|
@ -30,7 +59,10 @@ def get_location(msg):
|
||||||
base = db.get_path()
|
base = db.get_path()
|
||||||
assert path.startswith(base)
|
assert path.startswith(base)
|
||||||
path = path[len(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'}
|
assert state in {'cur', 'tmp', 'new'}
|
||||||
return (mailbox, folder, state)
|
return (mailbox, folder, state)
|
||||||
|
|
||||||
|
@ -38,10 +70,7 @@ MAILBOX_COLORS = dict()
|
||||||
|
|
||||||
def get_mailbox_color(mailbox):
|
def get_mailbox_color(mailbox):
|
||||||
if mailbox not in MAILBOX_COLORS:
|
if mailbox not in MAILBOX_COLORS:
|
||||||
colorfile = os.path.join(db.get_path(), mailbox, 'color')
|
colorStr = config[mailbox]["color"]
|
||||||
assert os.path.isfile(colorfile)
|
|
||||||
with open(colorfile, 'r') as f:
|
|
||||||
colorStr = f.read()
|
|
||||||
colorStr = colorStr[1:] if colorStr[0] == '#' else colorStr
|
colorStr = colorStr[1:] if colorStr[0] == '#' else colorStr
|
||||||
R = int(colorStr[0:2], 16)
|
R = int(colorStr[0:2], 16)
|
||||||
G = int(colorStr[2:4], 16)
|
G = int(colorStr[2:4], 16)
|
||||||
|
@ -57,14 +86,50 @@ def format_date(date):
|
||||||
else:
|
else:
|
||||||
return date.strftime('%d/%m/%y')
|
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):
|
def print_msg(msg):
|
||||||
|
if not destWidth:
|
||||||
|
compute_line_format()
|
||||||
|
|
||||||
line = ""
|
line = ""
|
||||||
tags = set(msg.get_tags())
|
tags = set(msg.get_tags())
|
||||||
mailbox, folder, state = get_location(msg)
|
mailbox, folder, state = get_location(msg)
|
||||||
line += get_mailbox_color(mailbox)
|
line += get_mailbox_color(mailbox)
|
||||||
|
|
||||||
|
# ID
|
||||||
|
line += threadIdToB64(msg.get_thread_id())
|
||||||
|
# line += str(int(msg.get_thread_id(), 16))
|
||||||
|
|
||||||
# Date
|
# Date
|
||||||
|
line += " "
|
||||||
date = datetime.datetime.fromtimestamp(msg.get_date())
|
date = datetime.datetime.fromtimestamp(msg.get_date())
|
||||||
line += format_date(date)
|
line += format_date(date)
|
||||||
|
|
||||||
|
@ -83,16 +148,23 @@ def print_msg(msg):
|
||||||
else:
|
else:
|
||||||
line += none
|
line += none
|
||||||
|
|
||||||
tags2col1('spam', 'draft', '??', '💥', '📝', ' ')
|
tags2col1('spam', 'draft', '?', 'S', 'D', ' ')
|
||||||
tags2col1('attachment', 'encrypted', '🔐', '📎', '🔑', ' ')
|
tags2col1('attachment', 'encrypted', 'E', 'A', 'E', ' ')
|
||||||
tags2col1('unread', 'flagged', '🏁', '🏳 ', '🏴', ' ')
|
tags2col1('unread', 'flagged', '!', 'U', 'F', ' ')
|
||||||
tags2col1('sent', 'replied', '?', '↑', '↪', ' ')
|
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
|
# Subject
|
||||||
line += " "
|
line += " "
|
||||||
line += msg.get_header("subject")
|
subject = msg.get_header("subject")
|
||||||
|
line += clip_text(subjectWidth, subject)
|
||||||
|
|
||||||
line += colorama.Style.RESET_ALL
|
line += colorama.Style.RESET_ALL
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
|
@ -102,26 +174,33 @@ def retag_msg(msg):
|
||||||
mailbox, folder, state = get_location(msg)
|
mailbox, folder, state = get_location(msg)
|
||||||
|
|
||||||
# Search-friendly folder name
|
# Search-friendly folder name
|
||||||
if folder.startswith('INBOX.'):
|
slugFolderList = list()
|
||||||
folder = folder[6:]
|
for f, fold in [(f, folder[f]) for f in range(len(folder))]:
|
||||||
folder = folder.upper()
|
if f == 0 and len(folder) > 1 and fold == "INBOX":
|
||||||
|
continue
|
||||||
|
slugFolderList.append(fold.upper())
|
||||||
|
slugFolder = tuple(slugFolderList)
|
||||||
|
|
||||||
msg.remove_all_tags()
|
tags = set(msg.get_tags())
|
||||||
if folder.startswith('JUNK') or folder.startswith('SPAM'):
|
def tag_if(tag, condition):
|
||||||
msg.add_tag('spam')
|
if condition and tag not in tags:
|
||||||
if folder.startswith('DRAFT'):
|
msg.add_tag(tag)
|
||||||
msg.add_tag('draft')
|
elif not condition and tag in tags:
|
||||||
if folder.startswith('INBOX'):
|
msg.remove_tag(tag)
|
||||||
msg.add_tag('inbox')
|
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
|
# Save
|
||||||
msg.thaw()
|
msg.thaw()
|
||||||
msg.tags_to_maildir_flags()
|
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 = notmuch.Query(db, queryStr)
|
||||||
query.set_sort(notmuch.Query.SORT.OLDEST_FIRST)
|
query.set_sort(notmuch.Query.SORT.OLDEST_FIRST)
|
||||||
msgs = query.search_messages()
|
msgs = query.search_messages()
|
||||||
|
@ -131,11 +210,23 @@ def applyMsgs(queryStr, function, useProgressbar=False):
|
||||||
else:
|
else:
|
||||||
iterator = msgs
|
iterator = msgs
|
||||||
for msg in iterator:
|
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:inbox', print_msg)
|
||||||
# applyMsgs('tag:spam', print_msg)
|
# applyMsgs('tag:spam', print_msg)
|
||||||
# applyMsgs('tag:unread', print_msg)
|
# applyMsgs('tag:unread', print_msg)
|
||||||
|
# applyMsgs('tag:unprocessed', print_msg)
|
||||||
# applyMsgs('from:geoffrey@frogeye.fr', print_msg)
|
# applyMsgs('from:geoffrey@frogeye.fr', print_msg)
|
||||||
|
|
||||||
|
# applyMsgs('tag:unprocessed', retag_msg, useProgressbar=True)
|
||||||
# applyMsgs('*', retag_msg, useProgressbar=True)
|
# applyMsgs('*', retag_msg, useProgressbar=True)
|
||||||
|
|
||||||
|
|
|
@ -9,16 +9,18 @@ import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# TODO Find config file from XDG
|
# TODO Find config file from XDG
|
||||||
# TODO Alias adresses
|
|
||||||
# TODO Signature file
|
# TODO Signature file
|
||||||
# TODO Write ~/.mail/[mailbox]/color file if required by sth?
|
# 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 = configparser.ConfigParser()
|
||||||
config.read(configPath)
|
config.read(configPath)
|
||||||
|
|
||||||
|
storageFull = os.path.realpath(os.path.expanduser(config["GENERAL"]["storage"]))
|
||||||
|
config["GENERAL"]["storage"] = storageFull
|
||||||
|
|
||||||
SERVER_DEFAULTS = {
|
SERVER_DEFAULTS = {
|
||||||
"imap": {"port": 143, "starttls": True},
|
"imap": {"port": 143, "starttls": True},
|
||||||
"smtp": {"port": 587, "starttls": True},
|
"smtp": {"port": 587, "starttls": True},
|
||||||
|
@ -27,6 +29,7 @@ SERVER_ITEMS = {"host", "port", "user", "pass", "starttls"}
|
||||||
|
|
||||||
# Reading sections
|
# Reading sections
|
||||||
accounts = dict()
|
accounts = dict()
|
||||||
|
mails = set()
|
||||||
|
|
||||||
for name in config.sections():
|
for name in config.sections():
|
||||||
if not name.islower():
|
if not name.islower():
|
||||||
|
@ -56,13 +59,23 @@ for name in config.sections():
|
||||||
continue
|
continue
|
||||||
data[key] = section[key]
|
data[key] = section[key]
|
||||||
|
|
||||||
data["name"] = name
|
mails.add(section["from"])
|
||||||
data["storage"] = os.path.join(config['GLOBAL']['storage'], name)
|
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")
|
data["storageInbox"] = os.path.join(data["storage"], "INBOX")
|
||||||
storageFull = os.path.expanduser(data["storage"])
|
|
||||||
os.makedirs(storageFull, exist_ok=True)
|
|
||||||
accounts[name] = data
|
accounts[name] = data
|
||||||
|
|
||||||
|
general = dict()
|
||||||
|
section = config["GENERAL"]
|
||||||
|
for key in section.keys():
|
||||||
|
general[key] = section[key]
|
||||||
|
general["main"] = accounts[general["main"]]
|
||||||
|
|
||||||
|
|
||||||
# OfflineIMAP
|
# OfflineIMAP
|
||||||
|
|
||||||
OFFLINEIMAP_BEGIN = """[general]
|
OFFLINEIMAP_BEGIN = """[general]
|
||||||
|
@ -82,19 +95,19 @@ footer = "\\n"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
OFFLINEIMAP_ACCOUNT = """[Account {name}]
|
OFFLINEIMAP_ACCOUNT = """[Account {account}]
|
||||||
localrepository = {name}-local
|
localrepository = {account}-local
|
||||||
remoterepository = {name}-remote
|
remoterepository = {account}-remote
|
||||||
autorefresh = 0.5
|
autorefresh = 0.5
|
||||||
quick = 10
|
quick = 10
|
||||||
utf8foldernames = yes
|
utf8foldernames = yes
|
||||||
postsynchook = ~/.mutt/postsync
|
postsynchook = ~/.mutt/postsync
|
||||||
|
|
||||||
[Repository {name}-local]
|
[Repository {account}-local]
|
||||||
type = Maildir
|
type = Maildir
|
||||||
localfolders = {storage}
|
localfolders = {storage}
|
||||||
|
|
||||||
[Repository {name}-remote]
|
[Repository {account}-remote]
|
||||||
type = IMAP
|
type = IMAP
|
||||||
{secconf}
|
{secconf}
|
||||||
keepalive = 60
|
keepalive = 60
|
||||||
|
@ -116,23 +129,24 @@ for name, account in accounts.items():
|
||||||
# TODO Write
|
# TODO Write
|
||||||
|
|
||||||
# mbsync
|
# mbsync
|
||||||
MBSYNC_ACCOUNT = """IMAPAccount {name}
|
MBSYNC_ACCOUNT = """IMAPAccount {account}
|
||||||
Host {imaphost}
|
Host {imaphost}
|
||||||
|
Port {imapport}
|
||||||
User {imapuser}
|
User {imapuser}
|
||||||
Pass "{imappass}"
|
Pass "{imappassEscaped}"
|
||||||
{secconf}
|
{secconf}
|
||||||
|
|
||||||
IMAPStore {name}-remote
|
IMAPStore {account}-remote
|
||||||
Account {name}
|
Account {account}
|
||||||
|
|
||||||
MaildirStore {name}-local
|
MaildirStore {account}-local
|
||||||
Subfolders Verbatim
|
Subfolders Verbatim
|
||||||
Path {storage}/
|
Path {storage}/
|
||||||
Inbox {storageInbox}/
|
Inbox {storageInbox}/
|
||||||
|
|
||||||
Channel {name}
|
Channel {account}
|
||||||
Master :{name}-remote:
|
Master :{account}-remote:
|
||||||
Slave :{name}-local:
|
Slave :{account}-local:
|
||||||
Patterns *
|
Patterns *
|
||||||
Create Both
|
Create Both
|
||||||
SyncState *
|
SyncState *
|
||||||
|
@ -145,7 +159,10 @@ for name, account in accounts.items():
|
||||||
secconf = "SSLType STARTTLS"
|
secconf = "SSLType STARTTLS"
|
||||||
else:
|
else:
|
||||||
secconf = "SSLType IMAPS"
|
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')
|
msbsyncFilepath = os.path.join(os.path.expanduser('~'), '.mbsyncrc')
|
||||||
with open(msbsyncFilepath, 'w') as f:
|
with open(msbsyncFilepath, 'w') as f:
|
||||||
f.write(mbsyncStr)
|
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}
|
from {from}
|
||||||
user {smtpuser}
|
user {smtpuser}
|
||||||
password {smtppass}
|
password {smtppass}
|
||||||
|
@ -174,3 +191,37 @@ for name, account in accounts.items():
|
||||||
msbsyncFilepath = os.path.join(os.path.expanduser('~'), '.msmtprc')
|
msbsyncFilepath = os.path.join(os.path.expanduser('~'), '.msmtprc')
|
||||||
with open(msbsyncFilepath, 'w') as f:
|
with open(msbsyncFilepath, 'w') as f:
|
||||||
f.write(msmtpStr)
|
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