org-mairix.el 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. ;;; org-mairix.el - Support for hooking mairix search into Org for different MUAs
  2. ;;
  3. ;; Copyright (C) 2007-2014 Georg C. F. Greve
  4. ;; mutt support by Adam Spiers <orgmode at adamspiers dot org>
  5. ;;
  6. ;; This file is not part of GNU Emacs.
  7. ;;
  8. ;; Author: Georg C. F. Greve <greve at fsfeurope dot org>
  9. ;; Keywords: outlines, hypermedia, calendar, wp, email, mairix
  10. ;; Purpose: Integrate mairix email searching into Org mode
  11. ;; See https://orgmode.org and http://www.rpcurnow.force9.co.uk/mairix/
  12. ;; Version: 0.5
  13. ;;
  14. ;; This file is Free Software; you can redistribute it and/or modify
  15. ;; it under the terms of the GNU General Public License as published by
  16. ;; the Free Software Foundation; either version 3, or (at your option)
  17. ;; any later version.
  18. ;; It is distributed in the hope that it will be useful, but WITHOUT
  19. ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  20. ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
  21. ;; License for more details.
  22. ;; You should have received a copy of the GNU General Public License
  23. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  24. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  25. ;; USAGE NOTE
  26. ;;
  27. ;; You will need to configure mairix first, which involves setting up your
  28. ;; .mairixrc in your home directory. Once it is working, you should set up
  29. ;; your way to display results in your favorite way -- usually a MUA.
  30. ;; Currently gnus and mutt are supported.
  31. ;;
  32. ;; After both steps are done, all you should need to hook mairix, org
  33. ;; and your MUA together is to do (require 'org-mairix) in your
  34. ;; startup file. Everything can then be configured normally through
  35. ;; Emacs customisation.
  36. ;;
  37. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  38. (require 'org)
  39. ;;; The custom variables
  40. (defgroup org-mairix nil
  41. "Mairix support/integration in org."
  42. :tag "Org Mairix"
  43. :group 'org)
  44. (defcustom org-mairix-threaded-links t
  45. "Should new links be created as threaded links?
  46. If t, links will be stored as threaded searches.
  47. If nil, links will be stored as non-threaded searches."
  48. :group 'org-mairix
  49. :type 'boolean)
  50. (defcustom org-mairix-augmented-links nil
  51. "Should new links be created as augmenting searches?
  52. If t, links will be stored as augmenting searches.
  53. If nil, links will be stored as normal searches.
  54. Attention: When activating this option, you will need
  55. to remove old articles from your mairix results group
  56. in some other way, mairix will not do it for you."
  57. :group 'org-mairix
  58. :type 'boolean)
  59. (defcustom org-mairix-display-hook 'org-mairix-gnus-display-results
  60. "Hook to call to display the results of a successful mairix search.
  61. Defaults to Gnus, feel free to add your own MUAs or methods."
  62. :group 'org-mairix
  63. :type 'hook)
  64. (defcustom org-mairix-open-command "mairix %args% '%search%'"
  65. "The mairix command-line to use. If your paths are set up
  66. correctly, you should not need to change this.
  67. '%search%' will get substituted with the search expression, and
  68. '%args%' with any additional arguments."
  69. :group 'org-mairix
  70. :type 'string)
  71. ;;; The hooks to integrate mairix into org
  72. (org-link-set-parameters "mairix"
  73. :follow #'org-mairix-open
  74. :store #'org-mairix-store-gnus-link)
  75. ;;; Generic org-mairix functions
  76. (defun org-mairix-construct-link (message-id)
  77. "Construct a mairix: hyperlink based on message-id."
  78. (concat "mairix:"
  79. (if org-mairix-threaded-links "t:")
  80. (if org-mairix-augmented-links "a:")
  81. "@@"
  82. (org-unbracket-string "<" ">" message-id)))
  83. (defun org-store-mairix-link-props (&rest plist)
  84. "Take a property list describing a mail, and add mairix link
  85. and description properties so that org can build a mairix link to
  86. it."
  87. ;; We have to call `org-store-link-props' twice:
  88. ;;
  89. ;; - It extracts 'fromname'/'fromaddress' from 'from' property,
  90. ;; and stores the updated plist to `org-store-link-plist'.
  91. ;;
  92. ;; - `org-email-link-description' uses these new properties to
  93. ;; build a description from the previously stored plist. I
  94. ;; wrote a tiny patch to `org-email-link-description' so it
  95. ;; could take a non-stored plist as an optional 2nd argument,
  96. ;; but the plist provided still needs 'fromname'/'fromaddress'.
  97. ;;
  98. ;; - Ideally we would decouple the storing bit of
  99. ;; `org-store-link-props' from the extraction bit, but lots of
  100. ;; stuff in `org-store-link' which calls it would need to be
  101. ;; changed. Maybe just factor out the extraction so it can be
  102. ;; reused separately?
  103. (let ((mid (plist-get plist :message-id)))
  104. (apply 'org-store-link-props
  105. (append plist
  106. (list :type "mairix"
  107. :link (org-mairix-construct-link mid))))
  108. (apply 'org-store-link-props
  109. (append org-store-link-plist
  110. (list :description (org-email-link-description))))))
  111. (defun org-mairix-message-send-and-exit-with-link ()
  112. "Function that can be assigned as an alternative sending function,
  113. it sends the message and then stores a mairix link to it before burying
  114. the buffer just like 'message-send-and-exit' does."
  115. (interactive)
  116. (message-send)
  117. (let* ((message-id (message-fetch-field "Message-Id"))
  118. (subject (message-fetch-field "Subject"))
  119. (link (org-mairix-construct-link message-id))
  120. (desc (concat "Email: '" subject "'")))
  121. (setq org-stored-links
  122. (cons (list link desc) org-stored-links)))
  123. (message-bury (current-buffer)))
  124. (defun org-mairix-open (search _)
  125. "Function to open mairix link.
  126. We first need to split it into its individual parts, and then
  127. extract the message-id to be passed on to the display function
  128. before call mairix, evaluate the number of matches returned, and
  129. make sure to only call display of mairix succeeded in matching."
  130. (let* ((args ""))
  131. (if (equal (substring search 0 2) "t:" )
  132. (progn (setq search (substring search 2 nil))
  133. (setq args (concat args " --threads"))))
  134. (if (equal (substring search 0 2) "a:")
  135. (progn (setq search (substring search 2 nil))
  136. (setq args (concat args " --augment"))))
  137. (let ((cmdline (org-mairix-command-substitution
  138. org-mairix-open-command search args)))
  139. (print cmdline)
  140. (setq retval (shell-command-to-string cmdline))
  141. (string-match "\[0-9\]+" retval)
  142. (setq matches (string-to-number (match-string 0 retval)))
  143. (if (eq matches 0) (message "Link failed: no matches, sorry")
  144. (message "Link returned %d matches" matches)
  145. (run-hook-with-args 'org-mairix-display-hook search args)))))
  146. (defun org-mairix-command-substitution (cmd search args)
  147. "Substitute '%search%' and '%args% in mairix search command."
  148. (while (string-match "%search%" cmd)
  149. (setq cmd (replace-match search 'fixedcase 'literal cmd)))
  150. (while (string-match "%args%" cmd)
  151. (setq cmd (replace-match args 'fixedcase 'literal cmd)))
  152. cmd)
  153. ;;; Functions necessary for integration of external MUAs.
  154. ;; Of course we cannot call `org-store-link' from within an external
  155. ;; MUA, so we need some other way of storing a link for later
  156. ;; retrieval by org-mode and/or remember-mode. To do this we use a
  157. ;; temporary file as a kind of dedicated clipboard.
  158. (defcustom org-mairix-link-clipboard "~/.org-mairix-link"
  159. "Pseudo-clipboard file where mairix URLs get copied to by external
  160. applications in order to mimic `org-store-link'. Used by
  161. `org-mairix-insert-link'."
  162. :group 'org-mairix
  163. :type 'string)
  164. ;; When we resolve some of the issues with `org-store-link' detailed
  165. ;; at <https://orgmode.org/list/20071105181739.GB13544@atlantic.linksys.moosehall
  166. ;; we might not need org-mairix-insert-link.
  167. (defun org-mairix-insert-link ()
  168. "Insert link from file defined by `org-mairix-link-clipboard'."
  169. (interactive)
  170. (let ((bytes (cadr (insert-file-contents
  171. (expand-file-name org-mairix-link-clipboard)))))
  172. (forward-char bytes)
  173. (save-excursion
  174. (forward-char -1)
  175. (if (looking-at "\n")
  176. (delete-char 1)))))
  177. ;;; Functions necessary for mutt integration
  178. (defgroup org-mairix-mutt nil
  179. "Use mutt for mairix support in org."
  180. :tag "Org Mairix Mutt"
  181. :group 'org-mairix)
  182. (defcustom org-mairix-mutt-display-command
  183. "xterm -title 'mairix search: %search%' -e 'unset COLUMNS; mutt -f
  184. ~/mail/mairix -e \"push <display-message>\"' &"
  185. "Command to execute to display mairix search results via mutt within
  186. an xterm.
  187. '%search%' will get substituted with the search expression, and
  188. '%args%' with any additional arguments used in the search."
  189. :group 'org-mairix-mutt
  190. :type 'string)
  191. (defun org-mairix-mutt-display-results (search args)
  192. "Display results of mairix search in mutt, using the command line
  193. defined in `org-mairix-mutt-display-command'."
  194. ;; By default, async `shell-command' invocations display the temp
  195. ;; buffer, which is annoying here. We choose a deterministic
  196. ;; buffer name so we can hide it again immediately.
  197. ;; Note: `call-process' is synchronous so not useful here.
  198. (let ((cmd (org-mairix-command-substitution
  199. org-mairix-mutt-display-command search args))
  200. (tmpbufname (generate-new-buffer-name " *mairix-view*")))
  201. (shell-command cmd tmpbufname)
  202. (delete-windows-on (get-buffer tmpbufname))))
  203. ;;; Functions necessary for gnus integration
  204. (defgroup org-mairix-gnus nil
  205. "Use gnus for mairix support in org."
  206. :tag "Org Mairix Gnus"
  207. :group 'org-mairix)
  208. (defcustom org-mairix-gnus-results-group "nnmaildir:mairix"
  209. "The group that is configured to hold the mairix search results,
  210. which needs to be setup independently of the org-mairix integration,
  211. along with general mairix configuration."
  212. :group 'org-mairix-gnus
  213. :type 'string)
  214. (defcustom org-mairix-gnus-select-display-group-function
  215. 'org-mairix-gnus-select-display-group-function-gg
  216. "Hook to call to select the group that contains the matching articles.
  217. We should not need this, it is owed to a problem of gnus that people were
  218. not yet able to figure out, see
  219. http://article.gmane.org/gmane.emacs.gnus.general/65248
  220. http://article.gmane.org/gmane.emacs.gnus.general/65265
  221. http://article.gmane.org/gmane.emacs.gnus.user/9596
  222. for reference.
  223. It seems gnus needs a 'forget/ignore everything you think you
  224. know about that group' function. Volunteers?"
  225. :group 'org-mairix-gnus
  226. :type 'hook)
  227. (defun org-mairix-store-gnus-link ()
  228. "Store a link to the current gnus message as a Mairix search for its
  229. Message ID."
  230. ;; gnus integration
  231. (when (memq major-mode '(gnus-summary-mode gnus-article-mode))
  232. (and (eq major-mode 'gnus-article-mode) (gnus-article-show-summary))
  233. (let* ((article (gnus-summary-article-number))
  234. (header (gnus-summary-article-header article))
  235. (from (mail-header-from header))
  236. (message-id (mail-header-id header))
  237. (subject (gnus-summary-subject-string)))
  238. (org-store-mairix-link-props :from from
  239. :subject subject
  240. :message-id message-id))))
  241. (defun org-mairix-gnus-display-results (search args)
  242. "Display results of mairix search in Gnus.
  243. Note: This does not work as cleanly as I would like it to. The
  244. problem being that Gnus should simply reread the group cleanly,
  245. without remembering anything. At the moment it seems to be unable
  246. to do that -- so you're likely to see zombies floating around.
  247. If you can improve this, please do!"
  248. (if (not (equal (substring search 0 2) "m:" ))
  249. (error "org-mairix-gnus-display-results: display of search other than
  250. message-id not implemented yet"))
  251. (setq message-id (substring search 2 nil))
  252. (require 'gnus)
  253. (require 'gnus-sum)
  254. ;; FIXME: (bzg/gg) We might need to make sure gnus is running here,
  255. ;; and to start it in case it isn't running already. Does
  256. ;; anyone know a function to do that? It seems main org mode
  257. ;; does not do this, either.
  258. (funcall (cdr (assq 'gnus org-link-frame-setup)))
  259. (if gnus-other-frame-object (select-frame gnus-other-frame-object))
  260. ;; FIXME: This is horribly broken. Please see
  261. ;; http://article.gmane.org/gmane.emacs.gnus.general/65248
  262. ;; http://article.gmane.org/gmane.emacs.gnus.general/65265
  263. ;; http://article.gmane.org/gmane.emacs.gnus.user/9596
  264. ;; for reference.
  265. ;;
  266. ;; It seems gnus needs a "forget/ignore everything you think you
  267. ;; know about that group" function. Volunteers?
  268. ;;
  269. ;; For now different methods seem to work differently well for
  270. ;; different people. So we're playing hook-selection here to make
  271. ;; it easy to play around until we found a proper solution.
  272. (run-hook-with-args 'org-mairix-gnus-select-display-group-function)
  273. (gnus-summary-select-article
  274. nil t t (car (gnus-find-matching-articles "message-id" message-id))))
  275. (defun org-mairix-gnus-select-display-group-function-gg ()
  276. "Georg's hack to select a group that gnus (falsely) believes to be
  277. empty to then call rebuilding of the summary. It leaves zombies of
  278. old searches around, though."
  279. (gnus-group-quick-select-group 0 org-mairix-gnus-results-group)
  280. (gnus-group-clear-data)
  281. (gnus-summary-reselect-current-group t t))
  282. (defun org-mairix-gnus-select-display-group-function-bzg ()
  283. "This is the classic way the org mode is using, and it seems to be
  284. using better for Bastien, so it may work for you."
  285. (gnus-group-clear-data org-mairix-gnus-results-group)
  286. (gnus-group-read-group t nil org-mairix-gnus-results-group))
  287. (provide 'org-mairix)
  288. ;;; org-mairix.el ends here