211 lines
7.4 KiB
Hy
Executable File
211 lines
7.4 KiB
Hy
Executable File
#!/usr/bin/env hy
|
|
|
|
(import subprocess [Popen run PIPE DEVNULL])
|
|
(import email.parser :as parser)
|
|
(import shutil)
|
|
(import csv)
|
|
(import sys)
|
|
(import os)
|
|
|
|
(defclass MailInbox []
|
|
(setv maildir-path None)
|
|
(defn __init__ [self maildir-path]
|
|
(setv self.maildir-path maildir-path))
|
|
(defn get-new-messages [self folder]
|
|
(let [target-dir (+ self.maildir-path "/" folder "/new")]
|
|
(lfor file (os.listdir target-dir)
|
|
(MailMessage.from-file self (+ target-dir "/" file)))))
|
|
(defn find-by-uid [self uid [exclude-msgs None]]
|
|
(let [files (str.splitlines
|
|
(. (run ["rg" "-Fl" uid self.maildir-path]
|
|
:stdout PIPE :text True) stdout))
|
|
output #{}]
|
|
(when (is-not exclude-msgs None)
|
|
(for [excluded exclude-msgs]
|
|
(files.remove (os.path.abspath (excluded.get-full-path)))))
|
|
(for [file files]
|
|
(let [msg (MailMessage.from-file self file)]
|
|
(when (= msg.uid uid)
|
|
(output.add msg))))
|
|
output))
|
|
(defn mark-all-read [self uid [exclude-msgs None]]
|
|
(let [msgs (self.find-by-uid uid exclude-msgs)
|
|
num 0]
|
|
(for [msg msgs]
|
|
(+= num 1)
|
|
(msg.mark-read))
|
|
num))
|
|
(defn mark-all-unread [self uid [exclude-msgs None]]
|
|
(let [msgs (self.find-by-uid uid exclude-msgs)
|
|
num 0]
|
|
(for [msg msgs]
|
|
(+= num 1)
|
|
(msg.mark-unread))
|
|
num)))
|
|
|
|
(defclass MailMessage []
|
|
(setv inbox None
|
|
uid None
|
|
file None
|
|
folder None
|
|
sender None
|
|
subject None
|
|
flags None
|
|
read? False
|
|
attachment? False
|
|
new? False)
|
|
(defn __init__ [self inbox uid path sender subject attachment?]
|
|
(let [dir-path (os.path.dirname path)]
|
|
(setv self.inbox inbox
|
|
self.uid uid
|
|
self.file (os.path.basename path)
|
|
self.folder (os.path.relpath (os.path.dirname dir-path)
|
|
inbox.maildir-path)
|
|
self.sender sender
|
|
self.subject subject
|
|
self.flags (MailMessage.get-path-flags self.file)
|
|
self.read? (in "S" self.flags)
|
|
self.attachment? attachment?
|
|
self.new? (= (os.path.basename dir-path) "new"))))
|
|
(defn get-dir-path [self]
|
|
(+ self.inbox.maildir-path "/"
|
|
self.folder "/"
|
|
(if self.new? "new" "cur")))
|
|
(defn get-full-path [self]
|
|
(+ (self.get-dir-path) "/" self.file))
|
|
(defn move [self new-folder]
|
|
(let [clean-new-folder (MailMessage.-clean-folder new-folder)]
|
|
(when (!= self.folder clean-new-folder)
|
|
(shutil.move (self.get-full-path)
|
|
(+ self.inbox.maildir-path "/"
|
|
clean-new-folder "/"
|
|
(if self.new? "new" "cur") "/"
|
|
self.file))
|
|
(setv self.folder clean-new-folder))))
|
|
(defn process [self]
|
|
(when self.new?
|
|
(shutil.move (self.get-full-path)
|
|
(+ self.inbox.maildir-path "/"
|
|
self.folder
|
|
"/cur/"
|
|
self.file))
|
|
(setv self.new? False)))
|
|
(defn mark-read [self]
|
|
(when (not self.read?)
|
|
(self.flags.add "S")
|
|
(let [base-name (get self.file (slice (+ (self.file.rindex ",") 1)))
|
|
new-name (+ base-name (str.join "" self.flags))
|
|
dir-path (self.get-dir-path)]
|
|
(shutil.move (+ dir-path "/" self.file) (+ dir-path "/" new-name))
|
|
(setv self.file new-name
|
|
self.read? True))))
|
|
(defn mark-unread [self]
|
|
(when self.read?
|
|
(self.flags.remove "S")
|
|
(let [base-name (get self.file (slice (+ (self.file.rindex ",") 1)))
|
|
new-name (+ base-name (str.join "" self.flags))
|
|
dir-path (self.get-dir-path)]
|
|
(shutil.move (+ dir-path "/" self.file) (+ dir-path "/" new-name))
|
|
(setv self.file new-name
|
|
self.read? False))))
|
|
(defn -parse-from-address [header]
|
|
(try
|
|
(let [index (str.index header "<")]
|
|
(get header (slice 1 (- index 2))))
|
|
(except [ValueError]
|
|
header)))
|
|
(defn -clean-folder [folder]
|
|
(when (str.startswith folder "/")
|
|
(setv folder (get folder (slice 1))))
|
|
(when (str.endswith folder "/")
|
|
(setv folder (get folder (slice None -1))))
|
|
folder)
|
|
(defn -message-has-attachment [mail-obj]
|
|
(when (mail-obj.is_multipart)
|
|
(for [part (mail-obj.walk)]
|
|
(when (str.startswith (part.get "Content-Disposition") "attachment")
|
|
(return True))))
|
|
False)
|
|
(defn get-path-flags [path]
|
|
(set (get path (slice (+ (path.rindex ",") 1) None))))
|
|
(defn from-file [inbox path]
|
|
(with [file-obj (open path "r")]
|
|
(let [parse (parser.Parser)
|
|
mail-obj (parse.parse file-obj :headersonly True)]
|
|
(MailMessage inbox
|
|
(mail-obj.get "Message-Id")
|
|
path
|
|
(MailMessage.-parse-from-address (mail-obj.get "From"))
|
|
(mail-obj.get "Subject")
|
|
(MailMessage.-message-has-attachment mail-obj))))))
|
|
|
|
(defclass INotifyEvent []
|
|
(setv path None
|
|
flags #{}
|
|
name None)
|
|
(defn __init__ [self path flags name]
|
|
(setv self.path (if (path.endswith "/")
|
|
(get path (slice 0 -1))
|
|
path)
|
|
self.flags (set (flags.split ","))
|
|
self.name name))
|
|
(defn get-full-path [self]
|
|
(+ self.path "/" self.name))
|
|
(defn dir? [self]
|
|
(in "ISDIR" self.flags))
|
|
(defn mail-file? [self]
|
|
(and (not (self.dir?))
|
|
(in (os.path.basename self.path) ["new" "cur"])))
|
|
(defn __str__ [self]
|
|
(+ "Event("
|
|
self.path ","
|
|
(str self.flags) ","
|
|
self.name ")")))
|
|
|
|
(defn handle-mail-move [inbox from-event to-event]
|
|
(let [from-flags (MailMessage.get-path-flags from-event.name)
|
|
to-flags (MailMessage.get-path-flags to-event.name)]
|
|
(cond (and (in "S" from-flags) (not-in "S" to-flags))
|
|
(let [to-msg (MailMessage.from-file inbox (to-event.get-full-path))]
|
|
(inbox.mark-all-unread to-msg.uid :exclude-msgs [to-msg]))
|
|
(and (in "S" to-flags) (not-in "S" from-flags))
|
|
(let [to-msg (MailMessage.from-file inbox (to-event.get-full-path))]
|
|
(inbox.mark-all-read to-msg.uid :exclude-msgs [to-msg]))
|
|
True
|
|
0)))
|
|
|
|
(when (< (len sys.argv) 2)
|
|
(print "usage: mail-reader-daemon.hy <maildir>" :file sys.stderr)
|
|
(sys.exit 1))
|
|
|
|
(when (= (get sys.argv 1) "-h")
|
|
(print "usage: mail-reader-daemon.hy <maildir>")
|
|
(sys.exit 0))
|
|
|
|
(with [process (Popen ["inotifywait"
|
|
"-mrce" "MOVED_FROM,MOVED_TO"
|
|
(get sys.argv 1)]
|
|
:stdout PIPE
|
|
:stderr DEVNULL
|
|
:text True)]
|
|
(let [reader (csv.reader process.stdout)
|
|
inbox (MailInbox (get sys.argv 1))
|
|
skip-count 0
|
|
from-event None]
|
|
(for [csv-line reader]
|
|
(let [event (INotifyEvent #* csv-line)]
|
|
(setv from-event
|
|
(cond (> skip-count 0)
|
|
(do
|
|
(-= skip-count 1)
|
|
None)
|
|
(is from-event None)
|
|
event
|
|
True
|
|
(do
|
|
(when (from-event.mail-file?)
|
|
(setv skip-count (* 2 (handle-mail-move inbox
|
|
from-event
|
|
event))))
|
|
None)))))))
|