notify-mail.hy 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/usr/bin/env hy
  2. (import email.parser :as parser)
  3. (import subprocess [run CompletedProcess PIPE])
  4. (import threading [Thread])
  5. (import shutil)
  6. (import os)
  7. (import sys)
  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. (for [msg msgs]
  32. (msg.mark-read)))))
  33. (defclass MailMessage []
  34. (setv inbox None
  35. uid None
  36. file None
  37. folder None
  38. sender None
  39. subject None
  40. flags None
  41. read? False
  42. attachment? False
  43. new? False)
  44. (defn __init__ [self inbox uid path sender subject attachment?]
  45. (let [dir-path (os.path.dirname path)]
  46. (setv self.inbox inbox
  47. self.uid uid
  48. self.file (os.path.basename path)
  49. self.folder (os.path.relpath (os.path.dirname dir-path)
  50. inbox.maildir-path)
  51. self.sender sender
  52. self.subject subject
  53. self.flags (MailMessage.-get-path-flags self.file)
  54. self.read? (in "S" self.flags)
  55. self.attachment? attachment?
  56. self.new? (= (os.path.basename dir-path) "new"))))
  57. (defn get-dir-path [self]
  58. (+ self.inbox.maildir-path "/"
  59. self.folder "/"
  60. (if self.new? "new" "cur")))
  61. (defn get-full-path [self]
  62. (+ (self.get-dir-path) "/" self.file))
  63. (defn move [self new-folder]
  64. (let [clean-new-folder (MailMessage.-clean-folder new-folder)]
  65. (when (!= self.folder clean-new-folder)
  66. (shutil.move (self.get-full-path)
  67. (+ self.inbox.maildir-path "/"
  68. clean-new-folder "/"
  69. (if self.new? "new" "cur") "/"
  70. self.file))
  71. (setv self.folder clean-new-folder))))
  72. (defn process [self]
  73. (when self.new?
  74. (shutil.move (self.get-full-path)
  75. (+ self.inbox.maildir-path "/"
  76. self.folder
  77. "/cur/"
  78. self.file))
  79. (setv self.new? False)))
  80. (defn mark-read [self]
  81. (when (not self.read?)
  82. (self.flags.add "S")
  83. (let [base-name (get self.file (slice (+ (self.file.rindex ",") 1)))
  84. new-name (+ base-name (str.join "" self.flags))
  85. dir-path (self.get-dir-path)]
  86. (shutil.move (+ dir-path "/" self.file) (+ dir-path "/" new-name))
  87. (setv self.file new-name
  88. self.read? True))))
  89. (defn -parse-from-address [header]
  90. (try
  91. (let [index (str.index header "<")]
  92. (get header (slice 1 (- index 2))))
  93. (except [ValueError]
  94. header)))
  95. (defn -clean-folder [folder]
  96. (when (str.startswith folder "/")
  97. (setv folder (get folder (slice 1))))
  98. (when (str.endswith folder "/")
  99. (setv folder (get folder (slice None -1))))
  100. folder)
  101. (defn -get-path-flags [path]
  102. (set (get path (slice (+ (path.rindex ",") 1) None))))
  103. (defn -message-has-attachment [mail-obj]
  104. (when (mail-obj.is_multipart)
  105. (for [part (mail-obj.walk)]
  106. (when (str.startswith (part.get "Content-Disposition") "attachment")
  107. (return True))))
  108. False)
  109. (defn from-file [inbox path]
  110. (with [file-obj (open path "r")]
  111. (let [parse (parser.Parser)
  112. mail-obj (parse.parse file-obj :headersonly True)]
  113. (MailMessage inbox
  114. (mail-obj.get "Message-Id")
  115. path
  116. (MailMessage.-parse-from-address (mail-obj.get "From"))
  117. (mail-obj.get "Subject")
  118. (MailMessage.-message-has-attachment mail-obj))))))
  119. (defn notify-send [title desc [time 0] [actions []]]
  120. (let [cmd ["notify-send" title desc "-t" (str time)]]
  121. (for [action actions]
  122. (cmd.append "-A")
  123. (cmd.append action))
  124. (let [result (run cmd :stdout PIPE :text True)]
  125. (try
  126. (int result.stdout)
  127. (except [ValueError]
  128. None)))))
  129. (defn handle-message [msg]
  130. (msg.process)
  131. (when (not msg.read?)
  132. (match (notify-send (+ (if msg.attachment? "󰈙 " "")
  133. "New mail from " msg.sender)
  134. msg.subject
  135. :time 10000
  136. :actions ["Mark Read" "Delete"])
  137. 0 (do
  138. (msg.mark-read))
  139. ;;(msg.inbox.mark-all-read msg.uid :exclude-msgs [msg]))
  140. 1 (do
  141. (msg.mark-read)
  142. ;;(msg.inbox.mark-all-read msg.uid :exclude-msgs [msg])
  143. (msg.move "Trash")))))
  144. (when (< (len sys.argv) 2)
  145. (print "usage: notify-mail.hy <maildir>" :file sys.stderr)
  146. (sys.exit 1))
  147. (when (= (get sys.argv 1) "-h")
  148. (print "usage: notify-mail.hy <maildir>")
  149. (sys.exit 0))
  150. (let [mail-inbox (MailInbox (get sys.argv 1))
  151. new-msgs (mail-inbox.get-new-messages "Inbox")]
  152. (for [msg new-msgs]
  153. (Thread.start (Thread :target handle-message
  154. :args #(msg)))))