Simple LISP library to parse a maildir file into a usable structure. It can then be used to move messages around based on these rules. Developed to fulfill my personal itch, pull requests welcome!

mailfilter-parse.lisp 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. ;;;;Copyright (c) 2015, Lily Carpenter
  2. ;;;;All rights reserved.
  3. ;;;;
  4. ;;;;Redistribution and use in source and binary forms, with or without modification,
  5. ;;;;are permitted provided that the following conditions are met:
  6. ;;;;
  7. ;;;;* Redistributions of source code must retain the above copyright notice, this
  8. ;;;; list of conditions and the following disclaimer.
  9. ;;;;
  10. ;;;;* Redistributions in binary form must reproduce the above copyright notice, this
  11. ;;;; list of conditions and the following disclaimer in the documentation and/or
  12. ;;;; other materials provided with the distribution.
  13. ;;;;
  14. ;;;;* Neither the name of the Lily Carpenter nor the names of its
  15. ;;;; contributors may be used to endorse or promote products derived from
  16. ;;;; this software without specific prior written permission.
  17. ;;;;
  18. ;;;;THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. ;;;;ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. ;;;;WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. ;;;;DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  22. ;;;;ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. ;;;;(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. ;;;;LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  25. ;;;;ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. ;;;;(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. ;;;;SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. (defvar *commands* '("to"))
  29. (defvar *maildir* nil)
  30. ;; This function is pretty ugly but I was tired when I wrote it and just wanted it to work.
  31. ;; TODO: Clean this up
  32. (defun split-quoted (str)
  33. (let ((my-list nil)
  34. (my-string (make-array 0 :element-type 'character :fill-pointer 0 :adjustable t))
  35. (encountered-quote nil))
  36. (loop for c across str do
  37. (if (eq c #\")
  38. (setf encountered-quote (not encountered-quote)))
  39. (if (or (not (eq c #\Space)) encountered-quote)
  40. (vector-push-extend c my-string)
  41. (progn (setf my-list (append my-list (list my-string)))
  42. (setf my-string (make-array 0 :element-type 'character :fill-pointer 0 :adjustable t)))))
  43. (setf my-list (append my-list (list my-string)))))
  44. (defun empty-string? (str)
  45. (string= "" str))
  46. (defun check-start (line expected seperator)
  47. (if (or (empty-string? line) (empty-string? seperator))
  48. nil
  49. (string= expected (first (split-sequence:split-sequence seperator line)))))
  50. (defun test-for-maildir (line)
  51. (check-start line "MAILDIR" #\=))
  52. (defun test-for-if (line)
  53. (check-start line "if" #\())
  54. (defun test-for-end-if (line)
  55. (check-start line "}" #\newline))
  56. (defun loop-file-until (s test-func worker-func hash)
  57. (loop for line = (read-line s nil)
  58. while (and line (not (funcall test-func line)))
  59. do
  60. (funcall worker-func s line hash)))
  61. (defun convert-maildir-value (value)
  62. (pathname (remove #\" (cl-ppcre:regex-replace-all "\\${HOME}" value (namestring (user-homedir-pathname))))))
  63. (defun set-maildir (line hash)
  64. (let ((maildir (convert-maildir-value (car (last (split-sequence:split-sequence #\= line))))))
  65. (setf (gethash "maildir" hash) maildir)
  66. (setf *maildir* maildir)))
  67. (defun parse-line (s line hash)
  68. (if (test-for-if line)
  69. (read-mailfilter-if s line hash)
  70. (if (test-for-maildir line)
  71. (set-maildir line hash))))
  72. (defun test-for-command (line)
  73. (member (first(split-quoted (string-trim '(#\Space #\Tab) line))) *commands* :test #'equal))
  74. (defun clean-line (line)
  75. (let ((cleaned-line (string-trim '(#\Space #\Tab) line)))
  76. (cl-ppcre:regex-replace-all "\\$MAILDIR" cleaned-line (namestring *maildir*))))
  77. (defun hash-add-command (line hash)
  78. (let* ((cleaned-line (clean-line line))
  79. (line-list (split-quoted cleaned-line)))
  80. (setf (gethash "command" hash) (first line-list))
  81. (setf (gethash "args" hash) (subseq line-list 1))
  82. hash))
  83. (defun get-regex (line)
  84. (second (split-sequence:split-sequence #\/ line)))
  85. (defun make-regex-hash (regex scanner)
  86. (let ((regex-hash (make-hash-table :test 'equal)))
  87. (setf (gethash "regex" regex-hash) (list regex scanner))
  88. regex-hash))
  89. (defun parse-if-line (line hash)
  90. (if (test-for-if line)
  91. (let* ((regex (get-regex line))
  92. (scanner (cl-ppcre:create-scanner regex))
  93. (regex-hash (make-regex-hash regex scanner)))
  94. (setf (gethash "patterns" hash) (append (gethash "patterns" hash) (list regex-hash)))
  95. regex-hash)))
  96. (defun parse-inner-if-line (s line hash)
  97. (declare (ignore s) )
  98. (if (test-for-command line)
  99. (hash-add-command line hash)))
  100. (defun read-mailfilter-if (s line hash)
  101. (let ((if-hash (parse-if-line line hash)))
  102. (loop-file-until s #'test-for-end-if #'parse-inner-if-line if-hash)))
  103. (defun read-mailfilter-file (file)
  104. (let ((hash (make-hash-table :test 'equal)))
  105. (with-open-file (s file)
  106. (loop-file-until s (lambda (x) (declare (ignore x))) #'parse-line hash))
  107. hash))
  108. (defun end-of-headers-p (line)
  109. (= (length line) 0))
  110. (defun message-match (message pattern)
  111. (let* ((scanner (cadr (gethash "regex" pattern)))
  112. (regex (car (gethash "regex" pattern)))
  113. (sb-impl::*default-external-format* :latin-1)
  114. (test))
  115. (with-open-stream (message-stream (mel:message-header-stream message))
  116. (loop for line = (read-line message-stream nil)
  117. while (and line (not test) (not (end-of-headers-p line)))
  118. do
  119. (setf test (cl-ppcre:scan scanner line))))
  120. test))
  121. (defun handle-command (command args message)
  122. (when (string= command "to")
  123. (let* ((pathname (pathname (concatenate 'string (remove #\" (car args)) "/")))
  124. (new-folder (mel:make-maildir-folder pathname :if-does-not-exist :create))
  125. (sb-impl::*default-external-format* :latin-1))
  126. (mel:move-message message new-folder))))
  127. (defun handle-message (message patterns)
  128. (labels ((my-message-match (pattern) (message-match message pattern)))
  129. (let ((result (find-if #'my-message-match patterns)))
  130. (when result
  131. (let ((command (gethash "command" result))
  132. (args (gethash "args" result)))
  133. (handle-command command args message))))))
  134. (defun filter-maildir-folder (mailfilter &optional src-folder)
  135. (let* ((mailfilter-rules (read-mailfilter-file mailfilter))
  136. (maildir (mel:make-maildir-folder (gethash "maildir" mailfilter-rules)))
  137. (src-folder (if src-folder
  138. (mel:make-maildir-folder src-folder)
  139. maildir))
  140. (patterns (gethash "patterns" mailfilter-rules))
  141. (messages (mel:messages src-folder)))
  142. (dolist (message messages)
  143. (handle-message message patterns))))