mail-reader-daemon.hy 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. #!/usr/bin/env hy
  2. (import subprocess [Popen run PIPE DEVNULL])
  3. (import email.parser :as parser)
  4. (import shutil)
  5. (import csv)
  6. (import sys)
  7. (import os)
  8. (defclass MailInbox []
  9. (setv maildir-path None)
  10. (defn __init__ [self maildir-path]
  11. (setv self.maildir-path maildir-path))
  12. (defn get-new-messages [self folder]
  13. (let [target-dir (+ self.maildir-path "/" folder "/new")]
  14. (lfor file (os.listdir target-dir)
  15. (MailMessage.from-file self (+ target-dir "/" file)))))
  16. (defn find-by-uid [self uid [exclude-msgs None]]
  17. (let [files (str.splitlines
  18. (. (run ["rg" "-Fl" uid self.maildir-path]
  19. :stdout PIPE :text True) stdout))
  20. output #{}]
  21. (when (is-not exclude-msgs None)
  22. (for [excluded exclude-msgs]
  23. (files.remove (os.path.abspath (excluded.get-full-path)))))
  24. (for [file files]
  25. (let [msg (MailMessage.from-file self file)]
  26. (when (= msg.uid uid)
  27. (output.add msg))))
  28. output))
  29. (defn mark-all-read [self uid [exclude-msgs None]]
  30. (let [msgs (self.find-by-uid uid exclude-msgs)
  31. num 0]
  32. (for [msg msgs]
  33. (+= num 1)
  34. (msg.mark-read))
  35. num))
  36. (defn mark-all-unread [self uid [exclude-msgs None]]
  37. (let [msgs (self.find-by-uid uid exclude-msgs)
  38. num 0]
  39. (for [msg msgs]
  40. (+= num 1)
  41. (msg.mark-unread))
  42. num)))
  43. (defclass MailMessage []
  44. (setv inbox None
  45. uid None
  46. file None
  47. folder None
  48. sender None
  49. subject None
  50. flags None
  51. read? False
  52. attachment? False
  53. new? False)
  54. (defn __init__ [self inbox uid path sender subject attachment?]
  55. (let [dir-path (os.path.dirname path)]
  56. (setv self.inbox inbox
  57. self.uid uid
  58. self.file (os.path.basename path)
  59. self.folder (os.path.relpath (os.path.dirname dir-path)
  60. inbox.maildir-path)
  61. self.sender sender
  62. self.subject subject
  63. self.flags (MailMessage.get-path-flags self.file)
  64. self.read? (in "S" self.flags)
  65. self.attachment? attachment?
  66. self.new? (= (os.path.basename dir-path) "new"))))
  67. (defn get-dir-path [self]
  68. (+ self.inbox.maildir-path "/"
  69. self.folder "/"
  70. (if self.new? "new" "cur")))
  71. (defn get-full-path [self]
  72. (+ (self.get-dir-path) "/" self.file))
  73. (defn move [self new-folder]
  74. (let [clean-new-folder (MailMessage.-clean-folder new-folder)]
  75. (when (!= self.folder clean-new-folder)
  76. (shutil.move (self.get-full-path)
  77. (+ self.inbox.maildir-path "/"
  78. clean-new-folder "/"
  79. (if self.new? "new" "cur") "/"
  80. self.file))
  81. (setv self.folder clean-new-folder))))
  82. (defn process [self]
  83. (when self.new?
  84. (shutil.move (self.get-full-path)
  85. (+ self.inbox.maildir-path "/"
  86. self.folder
  87. "/cur/"
  88. self.file))
  89. (setv self.new? False)))
  90. (defn mark-read [self]
  91. (when (not self.read?)
  92. (self.flags.add "S")
  93. (let [base-name (get self.file (slice (+ (self.file.rindex ",") 1)))
  94. new-name (+ base-name (str.join "" self.flags))
  95. dir-path (self.get-dir-path)]
  96. (shutil.move (+ dir-path "/" self.file) (+ dir-path "/" new-name))
  97. (setv self.file new-name
  98. self.read? True))))
  99. (defn mark-unread [self]
  100. (when self.read?
  101. (self.flags.remove "S")
  102. (let [base-name (get self.file (slice (+ (self.file.rindex ",") 1)))
  103. new-name (+ base-name (str.join "" self.flags))
  104. dir-path (self.get-dir-path)]
  105. (shutil.move (+ dir-path "/" self.file) (+ dir-path "/" new-name))
  106. (setv self.file new-name
  107. self.read? False))))
  108. (defn -parse-from-address [header]
  109. (try
  110. (let [index (str.index header "<")]
  111. (get header (slice 1 (- index 2))))
  112. (except [ValueError]
  113. header)))
  114. (defn -clean-folder [folder]
  115. (when (str.startswith folder "/")
  116. (setv folder (get folder (slice 1))))
  117. (when (str.endswith folder "/")
  118. (setv folder (get folder (slice None -1))))
  119. folder)
  120. (defn -message-has-attachment [mail-obj]
  121. (when (mail-obj.is_multipart)
  122. (for [part (mail-obj.walk)]
  123. (when (str.startswith (part.get "Content-Disposition") "attachment")
  124. (return True))))
  125. False)
  126. (defn get-path-flags [path]
  127. (set (get path (slice (+ (path.rindex ",") 1) None))))
  128. (defn from-file [inbox path]
  129. (with [file-obj (open path "r")]
  130. (let [parse (parser.Parser)
  131. mail-obj (parse.parse file-obj :headersonly True)]
  132. (MailMessage inbox
  133. (mail-obj.get "Message-Id")
  134. path
  135. (MailMessage.-parse-from-address (mail-obj.get "From"))
  136. (mail-obj.get "Subject")
  137. (MailMessage.-message-has-attachment mail-obj))))))
  138. (defclass INotifyEvent []
  139. (setv path None
  140. flags #{}
  141. name None)
  142. (defn __init__ [self path flags name]
  143. (setv self.path (if (path.endswith "/")
  144. (get path (slice 0 -1))
  145. path)
  146. self.flags (set (flags.split ","))
  147. self.name name))
  148. (defn get-full-path [self]
  149. (+ self.path "/" self.name))
  150. (defn dir? [self]
  151. (in "ISDIR" self.flags))
  152. (defn mail-file? [self]
  153. (and (not (self.dir?))
  154. (in (os.path.basename self.path) ["new" "cur"])))
  155. (defn __str__ [self]
  156. (+ "Event("
  157. self.path ","
  158. (str self.flags) ","
  159. self.name ")")))
  160. (defn handle-mail-move [inbox from-event to-event]
  161. (let [from-flags (MailMessage.get-path-flags from-event.name)
  162. to-flags (MailMessage.get-path-flags to-event.name)]
  163. (cond (and (in "S" from-flags) (not-in "S" to-flags))
  164. (let [to-msg (MailMessage.from-file inbox (to-event.get-full-path))]
  165. (inbox.mark-all-unread to-msg.uid :exclude-msgs [to-msg]))
  166. (and (in "S" to-flags) (not-in "S" from-flags))
  167. (let [to-msg (MailMessage.from-file inbox (to-event.get-full-path))]
  168. (inbox.mark-all-read to-msg.uid :exclude-msgs [to-msg]))
  169. True
  170. 0)))
  171. (when (< (len sys.argv) 2)
  172. (print "usage: mail-reader-daemon.hy <maildir>" :file sys.stderr)
  173. (sys.exit 1))
  174. (when (= (get sys.argv 1) "-h")
  175. (print "usage: mail-reader-daemon.hy <maildir>")
  176. (sys.exit 0))
  177. (with [process (Popen ["inotifywait"
  178. "-mrce" "MOVED_FROM,MOVED_TO"
  179. (get sys.argv 1)]
  180. :stdout PIPE
  181. :stderr DEVNULL
  182. :text True)]
  183. (let [reader (csv.reader process.stdout)
  184. inbox (MailInbox (get sys.argv 1))
  185. skip-count 0
  186. from-event None]
  187. (for [csv-line reader]
  188. (let [event (INotifyEvent #* csv-line)]
  189. (setv from-event
  190. (cond (> skip-count 0)
  191. (do
  192. (-= skip-count 1)
  193. None)
  194. (is from-event None)
  195. event
  196. True
  197. (do
  198. (when (from-event.mail-file?)
  199. (setv skip-count (* 2 (handle-mail-move inbox
  200. from-event
  201. event))))
  202. None)))))))