123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- #!/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)))))))
|