thunderbird: add an action to open message

This commit is contained in:
Vincent Bernat 2022-06-12 18:38:14 +02:00
parent d71dd264c3
commit 3985ffa085

View file

@ -4,42 +4,49 @@
new messages. This should be a builtin feature in Thunderbird, but it new messages. This should be a builtin feature in Thunderbird, but it
is not.""" is not."""
# TODO: # This is quite basic. Notably, it relies on some quirks of the
# - handle emails received at the same second # Thunderbird mbox format where the From line contains a timestamp of
# - add an action to Open TB (thunderbird mid:....) # the received message and therefore can be used as a watermark as
# long as we don't receive more than 1 message per second.
import sys import sys
import argparse import argparse
import pyinotify
import os import os
import subprocess
import email.parser import email.parser
import email.header import email.header
from pydbus import SessionBus from pydbus import SessionBus
from gi.repository import GLib, Gio
parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__) parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__)
parser.add_argument("root", metavar="FILE", help="path to Thunderbird mail folder") parser.add_argument("root", metavar="FILE", help="path to Thunderbird mail folder")
parser.add_argument("folder", metavar="FOLDER", help="folder to monitor", nargs="+") parser.add_argument("folder", metavar="FOLDER", help="folder to monitor", nargs="+")
options = parser.parse_args() options = parser.parse_args()
watermarks = {}
notify = SessionBus().get(".Notifications") def decode(header):
return " ".join(
(
decoded.decode(charset or "ascii") if type(decoded) is bytes else decoded
for decoded, charset in email.header.decode_header(header)
)
)
class EventHandler(pyinotify.ProcessEvent): def process(path):
def process_IN_CLOSE_WRITE(self, event):
watermark = process(event.pathname, watermarks[event.pathname])
watermarks[event.pathname] = watermark
def process(path, watermark):
new_watermark = None new_watermark = None
watermark = watermarks.get(path)
count = 0
with open(path, "rb") as mbox: with open(path, "rb") as mbox:
marker = b"\nFrom " marker = b"\nFrom "
chunk = 1024 chunk = 1024
# Find next message from the end
mbox.seek(0, os.SEEK_END) mbox.seek(0, os.SEEK_END)
begin = mbox.tell() begin = mbox.tell()
while begin > 0: while begin > 0 and count < 10:
count += 1
# Look for the beginning of a message
while True: while True:
begin -= 1024 begin -= 1024
if begin < 0: if begin < 0:
@ -52,6 +59,7 @@ def process(path, watermark):
continue continue
begin = begin + idx begin = begin + idx
break break
# Look for the end of this message
end = begin + 1 end = begin + 1
while True: while True:
mbox.seek(end) mbox.seek(end)
@ -65,47 +73,60 @@ def process(path, watermark):
continue continue
end = end + idx end = end + idx
break break
# Check if we have hit our watermark
mbox.seek(begin + 1) mbox.seek(begin + 1)
message = mbox.read(end - begin).split(b"\n") message = mbox.read(end - begin).split(b"\n")
if message[0] == watermark: if message[0] == watermark:
return new_watermark return
if new_watermark is None: if new_watermark is None:
new_watermark = message[0] new_watermark = message[0]
watermarks[path] = new_watermark
if watermark is None: if watermark is None:
return new_watermark return
# Parse the message to extract subject, author and Message-ID
message = b"\n".join(message[1:]) message = b"\n".join(message[1:])
parsed = email.parser.BytesParser().parsebytes(message, headersonly=True) parsed = email.parser.BytesParser().parsebytes(message, headersonly=True)
subject = parsed.get("subject") subject = parsed.get("subject")
author = parsed.get("from") author = parsed.get("from")
messageid = parsed.get("message-id")
if subject is not None and author is not None: if subject is not None and author is not None:
subject, charset = email.header.decode_header(subject) actions = [] if messageid is None else [messageid.strip("<>"), "Open"]
if charset is not None:
subject = subject.decode(charset)
author, charset = email.header.decode_header(author)
if charset is not None:
author = author.decode(charset)
notify.Notify( notify.Notify(
"Thunderbird", "Thunderbird",
0, 0,
"thunderbird", "thunderbird",
f"Mail from {author}", f"Mail from {decode(author)}",
subject, decode(subject),
[], actions,
{}, {},
10000, 10000,
) )
return new_watermark
wm = pyinotify.WatchManager() notify = SessionBus().get(".Notifications")
mask = pyinotify.IN_CLOSE_WRITE monitors = []
handler = EventHandler() watermarks = {}
notifier = pyinotify.Notifier(wm, handler)
for folder in options.folder: for folder in options.folder:
folder = folder.split("/") folder = folder.split("/")
folder = [f"{f}.sbd" for f in folder[:-1]] + [folder[-1]] folder = [f"{f}.sbd" for f in folder[:-1]] + [folder[-1]]
folder = os.path.join(os.path.expanduser(options.root), *folder) folder = os.path.join(os.path.expanduser(options.root), *folder)
print(f"Watch {folder}...", flush=True) print(f"Watch {folder}...", flush=True)
watermarks[folder] = process(folder, None) # Take a not of the last message in the folder
wm.add_watch(folder, mask) process(folder)
notifier.loop() # Monitor it for change
gfile = Gio.File.new_for_path(folder)
monitor = gfile.monitor_file(Gio.FileMonitorFlags.NONE, None)
monitor.connect(
"changed",
lambda m, f1, f2, event: event == Gio.FileMonitorEvent.CHANGES_DONE_HINT
and process(folder),
)
monitors.append(monitor)
# Reply to notification actions
notify.ActionInvoked.connect(
lambda _, mid: subprocess.call(["thunderbird", f"mid:{mid}"])
)
GLib.MainLoop().run()