mairix.el 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  1. ;;; mairix.el --- Mairix interface for Emacs
  2. ;; Copyright (C) 2008-2012 Free Software Foundation, Inc.
  3. ;; Author: David Engster <dengste@eml.cc>
  4. ;; Keywords: mail searching
  5. ;; This file is part of GNU Emacs.
  6. ;; GNU Emacs is free software: you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; GNU Emacs is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;; This is an interface to the mairix mail search engine. Mairix is
  18. ;; written by Richard Curnow and is licensed under the GPL. See the
  19. ;; home page for details:
  20. ;;
  21. ;; http://www.rpcurnow.force9.co.uk/mairix/
  22. ;;
  23. ;; Features of mairix.el:
  24. ;;
  25. ;; * Query mairix with a search term.
  26. ;; * Currently supported Emacs mail programs: RMail, Gnus (mbox only),
  27. ;; and VM.
  28. ;; * Generate search queries using graphical widgets.
  29. ;; * Generate search queries based on currently displayed mail.
  30. ;; * Save regularly used searches in your .emacs customize section.
  31. ;; * Major mode for viewing, editing and querying saved searches.
  32. ;; * Update mairix database.
  33. ;;
  34. ;; Please note: There are currently no pre-defined key bindings, since
  35. ;; I guess these would depend on the used mail program. See the docs
  36. ;; for an overview of the provided interactive functions.
  37. ;;
  38. ;; Attention Gnus users: If you use Gnus with maildir or nnml, you
  39. ;; should use the native Gnus back end nnmairix.el instead, since it
  40. ;; has more features and is better integrated with Gnus. This
  41. ;; interface is essentially a stripped down version of nnmairix.el.
  42. ;;
  43. ;; Currently, RMail, Gnus (with mbox files), and VM are supported as
  44. ;; mail programs, but it is pretty easy to interface it with other
  45. ;; ones as well. Please see the docs and the source for details.
  46. ;; In a nutshell: include your favorite mail program in
  47. ;; `mairix-mail-program' and write functions for
  48. ;; `mairix-display-functions' and `mairix-get-mail-header-functions'.
  49. ;; If you have written such functions for your Emacs mail program of
  50. ;; choice, please let me know, so that I can eventually include them
  51. ;; in future version of mairix.el.
  52. ;;; History:
  53. ;; 07/28/2008: version 0.2. Added VM interface, written by Ulrich
  54. ;; Mueller.
  55. ;; 07/14/2008: Initial release
  56. ;;; Code:
  57. (require 'widget)
  58. (require 'cus-edit)
  59. (eval-when-compile
  60. (require 'cl))
  61. ;;; Keymappings
  62. ;; (currently none - please create them yourself)
  63. ;;; Customizable variables
  64. (defgroup mairix nil
  65. "Mairix interface for Emacs."
  66. :group 'mail)
  67. (defcustom mairix-file-path "~/"
  68. "Path where output files produced by Mairix should be stored."
  69. :type 'directory
  70. :group 'mairix)
  71. (defcustom mairix-search-file "mairixsearch.mbox"
  72. "Name of the default file for storing the searches.
  73. Note that this will be prefixed by `mairix-file-path'."
  74. :type 'string
  75. :group 'mairix)
  76. (defcustom mairix-command "mairix"
  77. "Command for calling mairix.
  78. You can add further options here if you want to, but better use
  79. `mairix-update-options' instead."
  80. :type 'string
  81. :group 'mairix)
  82. (defcustom mairix-output-buffer "*mairix output*"
  83. "Name of the buffer for the output of the mairix binary."
  84. :type 'string
  85. :group 'mairix)
  86. (defcustom mairix-customize-query-buffer "*mairix query*"
  87. "Name of the buffer for customizing a search query."
  88. :type 'string
  89. :group 'mairix)
  90. (defcustom mairix-saved-searches-buffer "*mairix searches*"
  91. "Name of the buffer for displaying saved searches."
  92. :type 'string
  93. :group 'mairix)
  94. (defcustom mairix-update-options '("-F" "-Q")
  95. "Options when calling mairix for updating the database.
  96. The default is '-F' and '-Q' for making updates faster. You
  97. should call mairix without these options from time to
  98. time (e.g. via cron job)."
  99. :type '(repeat string)
  100. :group 'mairix)
  101. (defcustom mairix-search-options '("-Q")
  102. "Options when calling mairix for searching.
  103. The default is '-Q' for making searching faster."
  104. :type '(repeat string)
  105. :group 'mairix)
  106. (defcustom mairix-synchronous-update nil
  107. "Defines if Emacs should wait for the mairix database update."
  108. :type 'boolean
  109. :group 'mairix)
  110. (defcustom mairix-saved-searches nil
  111. "Saved mairix searches.
  112. The entries are: Name of the search, Mairix query string, Name of
  113. the file (nil: use `mairix-search-file' as default), Search whole
  114. threads (nil or t). Note that the file will be prefixed by
  115. `mairix-file-path'."
  116. :type '(repeat (list (string :tag "Name")
  117. (string :tag "Query")
  118. (choice :tag "File"
  119. (const :tag "default")
  120. file)
  121. (boolean :tag "Threads")))
  122. :group 'mairix)
  123. (defcustom mairix-mail-program 'rmail
  124. "Mail program used to display search results.
  125. Currently RMail, Gnus (mbox), and VM are supported. If you use Gnus
  126. with maildir, use nnmairix.el instead."
  127. :type '(choice (const :tag "RMail" rmail)
  128. (const :tag "Gnus mbox" gnus)
  129. (const :tag "VM" vm))
  130. :group 'mairix)
  131. (defcustom mairix-display-functions
  132. '((rmail mairix-rmail-display)
  133. (gnus mairix-gnus-ephemeral-nndoc)
  134. (vm mairix-vm-display))
  135. "Specifies which function should be called for displaying search results.
  136. This is an alist where each entry consists of a symbol from
  137. `mairix-mail-program' and the corresponding function for
  138. displaying the search results. The function will be called with
  139. the mailbox file produced by mairix as the single argument."
  140. :type '(repeat (list (symbol :tag "Mail program")
  141. (function)))
  142. :group 'mairix)
  143. (defcustom mairix-get-mail-header-functions
  144. '((rmail mairix-rmail-fetch-field)
  145. (gnus mairix-gnus-fetch-field)
  146. (vm mairix-vm-fetch-field))
  147. "Specifies function for obtaining a header field from the current mail.
  148. This is an alist where each entry consists of a symbol from
  149. `mairix-mail-program' and the corresponding function for
  150. obtaining a header field from the current displayed mail. The
  151. function will be called with the mail header string as single
  152. argument. You can use nil if you do not have such a function for
  153. your mail program, but then searches based on the current mail
  154. won't work."
  155. :type '(repeat (list (symbol :tag "Mail program")
  156. (choice :tag "Header function"
  157. (const :tag "none")
  158. function)))
  159. :group 'mairix)
  160. (defcustom mairix-widget-select-window-function
  161. (lambda () (select-window (get-largest-window)))
  162. "Function for selecting the window for customizing the mairix query.
  163. The default chooses the largest window in the current frame."
  164. :type 'function
  165. :group 'mairix)
  166. ;; Other variables
  167. (defvar mairix-widget-fields-list
  168. '(("from" "f" "From") ("to" "t" "To") ("cc" "c" "Cc")
  169. ("subject" "s" "Subject") ("to" "tc" "To or Cc")
  170. ("from" "a" "Address") (nil "b" "Body") (nil "n" "Attachment")
  171. ("Message-ID" "m" "Message ID") (nil "s" "Size") (nil "d" "Date"))
  172. "Fields that should be editable during interactive query customization.
  173. Header, corresponding mairix command and description for editable
  174. fields in interactive query customization. The header specifies
  175. which header contents should be inserted into the editable field
  176. when creating a Mairix query based on the current message (can be
  177. nil for disabling this).")
  178. (defvar mairix-widget-other
  179. '(threads flags)
  180. "Other editable mairix commands when using customization widgets.
  181. Currently there are 'threads and 'flags.")
  182. ;;;; Internal variables
  183. (defvar mairix-last-search nil)
  184. (defvar mairix-searches-changed nil)
  185. ;;;; Interface functions for Emacs mail programs
  186. ;;; RMail
  187. ;; Display function:
  188. (autoload 'rmail "rmail")
  189. (autoload 'rmail-summary-displayed "rmail")
  190. (autoload 'rmail-summary "rmailsum")
  191. (defvar rmail-buffer)
  192. (defun mairix-rmail-display (folder)
  193. "Display mbox file FOLDER with RMail."
  194. (let (show-summary)
  195. ;; If it exists, select existing RMail window
  196. (when (and (boundp 'rmail-buffer)
  197. rmail-buffer)
  198. (set-buffer rmail-buffer)
  199. (when (get-buffer-window rmail-buffer)
  200. (select-window (get-buffer-window rmail-buffer))
  201. (setq show-summary (rmail-summary-displayed))))
  202. ;; check if folder is already open and if so, kill it
  203. (when (get-buffer (file-name-nondirectory folder))
  204. (set-buffer
  205. (get-buffer (file-name-nondirectory folder)))
  206. (set-buffer-modified-p nil)
  207. (kill-buffer nil))
  208. (rmail folder)
  209. ;; Update summary if necessary
  210. (when show-summary
  211. (rmail-summary))))
  212. ;; Fetching mail header field:
  213. (defun mairix-rmail-fetch-field (field)
  214. "Get mail header FIELD for current message using RMail."
  215. (unless (and (boundp 'rmail-buffer)
  216. rmail-buffer)
  217. (error "No RMail buffer available"))
  218. ;; At this point, we are in rmail mode, so the rmail funcs are loaded.
  219. (if (fboundp 'rmail-get-header) ; Emacs 23
  220. (rmail-get-header field)
  221. (with-current-buffer rmail-buffer
  222. (save-restriction
  223. ;; Don't warn about this when compiling Emacs 23.
  224. (with-no-warnings (rmail-narrow-to-non-pruned-header))
  225. (mail-fetch-field field)))))
  226. ;;; Gnus
  227. (eval-when-compile
  228. (defvar gnus-article-buffer)
  229. (autoload 'gnus-summary-toggle-header "gnus-sum")
  230. (autoload 'gnus-buffer-exists-p "gnus-util")
  231. (autoload 'message-field-value "message")
  232. (autoload 'gnus-group-read-ephemeral-group "gnus-group")
  233. (autoload 'gnus-alive-p "gnus-util"))
  234. ;; Display function:
  235. (defun mairix-gnus-ephemeral-nndoc (folder)
  236. "Create ephemeral nndoc group for reading mbox file FOLDER in Gnus."
  237. (unless (gnus-alive-p)
  238. (error "Gnus is not running"))
  239. (gnus-group-read-ephemeral-group
  240. ;; add randomness to group string to prevent Gnus from using a
  241. ;; cached version
  242. (format "mairix.%s" (number-to-string (random 10000)))
  243. `(nndoc "mairix"
  244. (nndoc-address ,folder)
  245. (nndoc-article-type mbox))))
  246. ;; Fetching mail header field:
  247. (defun mairix-gnus-fetch-field (field)
  248. "Get mail header FIELD for current message using Gnus."
  249. (unless (gnus-alive-p)
  250. (error "Gnus is not running"))
  251. (unless (gnus-buffer-exists-p gnus-article-buffer)
  252. (error "No article buffer available"))
  253. (with-current-buffer gnus-article-buffer
  254. (gnus-summary-toggle-header 1)
  255. (message-field-value field)))
  256. ;;; VM
  257. ;;; written by Ulrich Mueller
  258. (eval-when-compile
  259. (autoload 'vm-quit "vm-folder")
  260. (autoload 'vm-visit-folder "vm")
  261. (autoload 'vm-select-folder-buffer "vm-macro")
  262. (autoload 'vm-check-for-killed-summary "vm-misc")
  263. (autoload 'vm-get-header-contents "vm-summary")
  264. (autoload 'vm-check-for-killed-summary "vm-misc")
  265. (autoload 'vm-error-if-folder-empty "vm-misc")
  266. (autoload 'vm-select-marked-or-prefixed-messages "vm-folder"))
  267. ;; Display function
  268. (defun mairix-vm-display (folder)
  269. "Display mbox file FOLDER with VM."
  270. (require 'vm)
  271. ;; check if folder is already open and if so, kill it
  272. (let ((buf (get-file-buffer folder)))
  273. (when buf
  274. (set-buffer buf)
  275. (set-buffer-modified-p nil)
  276. (condition-case nil
  277. (vm-quit t)
  278. (error nil))
  279. (kill-buffer buf)))
  280. (vm-visit-folder folder t))
  281. ;; Fetching mail header field
  282. (defun mairix-vm-fetch-field (field)
  283. "Get mail header FIELD for current message using VM."
  284. (save-excursion
  285. (vm-select-folder-buffer)
  286. (vm-check-for-killed-summary)
  287. (vm-error-if-folder-empty)
  288. (vm-get-header-contents
  289. (car (vm-select-marked-or-prefixed-messages 1)) field)))
  290. ;;;; Main interactive functions
  291. (defun mairix-search (search threads)
  292. "Call Mairix with SEARCH.
  293. If THREADS is non-nil, also display whole threads of found
  294. messages. Results will be put into the default search file."
  295. (interactive
  296. (list
  297. (read-string "Query: ")
  298. (y-or-n-p "Include threads? ")))
  299. (when (mairix-call-mairix
  300. (split-string search)
  301. nil
  302. threads)
  303. (mairix-show-folder mairix-search-file)))
  304. (defun mairix-use-saved-search ()
  305. "Use a saved search for querying Mairix."
  306. (interactive)
  307. (let* ((completions
  308. (mapcar (lambda (el) (list (car el))) mairix-saved-searches))
  309. (search (completing-read "Name of search: " completions))
  310. (query (assoc search mairix-saved-searches))
  311. (folder (nth 2 query)))
  312. (when (not folder)
  313. (setq folder mairix-search-file))
  314. (when query
  315. (mairix-call-mairix
  316. (split-string (nth 1 query))
  317. folder
  318. (car (last query)))
  319. (mairix-show-folder folder))))
  320. (defun mairix-save-search ()
  321. "Save the last search."
  322. (interactive)
  323. (let* ((name (read-string "Name of the search: "))
  324. (exist (assoc name mairix-saved-searches)))
  325. (if (not exist)
  326. (add-to-list 'mairix-saved-searches
  327. (append (list name) mairix-last-search))
  328. (when
  329. (y-or-n-p
  330. "There is already a search with this name. \
  331. Overwrite existing entry? ")
  332. (setcdr (assoc name mairix-saved-searches) mairix-last-search))))
  333. (mairix-select-save))
  334. (defun mairix-edit-saved-searches-customize ()
  335. "Edit the list of saved searches in a customization buffer."
  336. (interactive)
  337. (custom-buffer-create (list (list 'mairix-saved-searches 'custom-variable))
  338. "*Customize Mairix Query*"
  339. (concat "\n\n" (make-string 65 ?=)
  340. "\nYou can now customize your saved Mairix searches by modifying\n\
  341. the variable mairix-saved-searches. Don't forget to save your\nchanges \
  342. in your .emacs by pressing 'Save for Future Sessions'.\n"
  343. (make-string 65 ?=) "\n")))
  344. (autoload 'mail-strip-quoted-names "mail-utils")
  345. (defun mairix-search-from-this-article (threads)
  346. "Search messages from sender of the current article.
  347. This is effectively a shortcut for calling `mairix-search' with
  348. f:current_from. If prefix THREADS is non-nil, include whole
  349. threads."
  350. (interactive "P")
  351. (let ((get-mail-header
  352. (cadr (assq mairix-mail-program mairix-get-mail-header-functions))))
  353. (if get-mail-header
  354. (mairix-search
  355. (format "f:%s"
  356. (mail-strip-quoted-names
  357. (funcall get-mail-header "from")))
  358. threads)
  359. (error "No function for obtaining mail header specified"))))
  360. (defun mairix-search-thread-this-article ()
  361. "Search thread for the current article.
  362. This is effectively a shortcut for calling `mairix-search'
  363. with m:msgid of the current article and enabled threads."
  364. (interactive)
  365. (let ((get-mail-header
  366. (cadr (assq mairix-mail-program mairix-get-mail-header-functions)))
  367. mid)
  368. (unless get-mail-header
  369. (error "No function for obtaining mail header specified"))
  370. (setq mid (funcall get-mail-header "message-id"))
  371. (while (string-match "[<>]" mid)
  372. (setq mid (replace-match "" t t mid)))
  373. ;; mairix somehow does not like '$' in message-id
  374. (when (string-match "\\$" mid)
  375. (setq mid (concat mid "=")))
  376. (while (string-match "\\$" mid)
  377. (setq mid (replace-match "=," t t mid)))
  378. (mairix-search
  379. (format "m:%s" mid) t)))
  380. (defun mairix-widget-search-based-on-article ()
  381. "Create mairix query based on current article using widgets."
  382. (interactive)
  383. (mairix-widget-search
  384. (mairix-widget-get-values)))
  385. (defun mairix-edit-saved-searches ()
  386. "Edit current mairix searches."
  387. (interactive)
  388. (switch-to-buffer mairix-saved-searches-buffer)
  389. (erase-buffer)
  390. (setq mairix-searches-changed nil)
  391. (mairix-build-search-list)
  392. (mairix-searches-mode)
  393. (hl-line-mode))
  394. (defvar mairix-widgets)
  395. (defun mairix-widget-search (&optional mvalues)
  396. "Create mairix query interactively using graphical widgets.
  397. MVALUES may contain values from current article."
  398. (interactive)
  399. ;; Select window for mairix customization
  400. (funcall mairix-widget-select-window-function)
  401. ;; generate widgets
  402. (mairix-widget-create-query mvalues)
  403. ;; generate Buttons
  404. (widget-create 'push-button
  405. :notify
  406. (lambda (&rest ignore)
  407. (mairix-widget-send-query mairix-widgets))
  408. "Send Query")
  409. (widget-insert " ")
  410. (widget-create 'push-button
  411. :notify
  412. (lambda (&rest ignore)
  413. (mairix-widget-save-search mairix-widgets))
  414. "Save search")
  415. (widget-insert " ")
  416. (widget-create 'push-button
  417. :notify (lambda (&rest ignore)
  418. (kill-buffer mairix-customize-query-buffer))
  419. "Cancel")
  420. (use-local-map widget-keymap)
  421. (widget-setup)
  422. (goto-char (point-min)))
  423. (defun mairix-update-database ()
  424. "Call mairix for updating the database for SERVERS.
  425. Mairix will be called asynchronously unless
  426. `mairix-synchronous-update' is t. Mairix will be called with
  427. `mairix-update-options'."
  428. (interactive)
  429. (let ((commandsplit (split-string mairix-command))
  430. args)
  431. (if mairix-synchronous-update
  432. (progn
  433. (setq args (append (list (car commandsplit) nil
  434. (get-buffer-create mairix-output-buffer)
  435. nil)))
  436. (if (> (length commandsplit) 1)
  437. (setq args (append args
  438. (cdr commandsplit)
  439. mairix-update-options))
  440. (setq args (append args mairix-update-options)))
  441. (apply 'call-process args))
  442. (progn
  443. (message "Updating mairix database...")
  444. (setq args (append (list "mairixupdate" (get-buffer-create mairix-output-buffer)
  445. (car commandsplit))))
  446. (if (> (length commandsplit) 1)
  447. (setq args (append args (cdr commandsplit) mairix-update-options))
  448. (setq args (append args mairix-update-options)))
  449. (set-process-sentinel
  450. (apply 'start-process args)
  451. 'mairix-sentinel-mairix-update-finished)))))
  452. ;;;; Helper functions
  453. (defun mairix-show-folder (folder)
  454. "Display mail FOLDER with mail program.
  455. The mail program is given by `mairix-mail-program'."
  456. (let ((display-function
  457. (cadr (assq mairix-mail-program mairix-display-functions))))
  458. (if display-function
  459. (funcall display-function
  460. (concat
  461. (file-name-as-directory
  462. (expand-file-name mairix-file-path))
  463. folder))
  464. (error "No mail program set"))))
  465. (defun mairix-call-mairix (query file threads)
  466. "Call Mairix with QUERY and output FILE.
  467. If FILE is nil, use default. If THREADS is non-nil, also return
  468. whole threads. Function returns t if messages were found."
  469. (let* ((commandsplit (split-string mairix-command))
  470. (args (cons (car commandsplit)
  471. `(nil ,(get-buffer-create mairix-output-buffer) nil)))
  472. rval)
  473. (with-current-buffer mairix-output-buffer
  474. (erase-buffer))
  475. (when (> (length commandsplit) 1)
  476. (setq args (append args (cdr commandsplit))))
  477. (when threads
  478. (setq args (append args '("-t"))))
  479. (when (stringp query)
  480. (setq query (split-string query)))
  481. (setq mairix-last-search (list (mapconcat 'identity query " ")
  482. file threads))
  483. (when (not file)
  484. (setq file mairix-search-file))
  485. (setq file
  486. (concat
  487. (file-name-as-directory
  488. (expand-file-name
  489. mairix-file-path))
  490. file))
  491. (setq rval
  492. (apply 'call-process
  493. (append args (list "-o" file) query)))
  494. (if (zerop rval)
  495. (with-current-buffer mairix-output-buffer
  496. (goto-char (point-min))
  497. (re-search-forward "^Matched.*messages")
  498. (message (match-string 0)))
  499. (if (and (= rval 1)
  500. (with-current-buffer mairix-output-buffer
  501. (goto-char (point-min))
  502. (looking-at "^Matched 0 messages")))
  503. (message "No messages found")
  504. (error "Error running Mairix. See buffer %s for details"
  505. mairix-output-buffer)))
  506. (zerop rval)))
  507. (defun mairix-replace-invalid-chars (header)
  508. "Replace invalid characters in HEADER for mairix query."
  509. (when header
  510. (while (string-match "[^-.@/,^=~& [:alnum:]]" header)
  511. (setq header (replace-match "" t t header)))
  512. (while (string-match "[& ]" header)
  513. (setq header (replace-match "," t t header)))
  514. header))
  515. (defun mairix-sentinel-mairix-update-finished (proc status)
  516. "Sentinel for mairix update process PROC with STATUS."
  517. (if (equal status "finished\n")
  518. (message "Updating mairix database... done")
  519. (error "There was an error updating the mairix database. \
  520. See %s for details" mairix-output-buffer)))
  521. ;;;; Widget stuff
  522. (defun mairix-widget-send-query (widgets)
  523. "Send query from WIDGETS to mairix binary."
  524. (mairix-search
  525. (mairix-widget-make-query-from-widgets widgets)
  526. (if (widget-value (cadr (assoc "Threads" widgets))) t))
  527. (kill-buffer mairix-customize-query-buffer))
  528. (defun mairix-widget-save-search (widgets)
  529. "Save search based on WIDGETS for future use."
  530. (let ((mairix-last-search
  531. `( ,(mairix-widget-make-query-from-widgets widgets)
  532. nil
  533. ,(widget-value (cadr (assoc "Threads" widgets))))))
  534. (mairix-save-search)
  535. (kill-buffer mairix-customize-query-buffer)))
  536. (defun mairix-widget-make-query-from-widgets (widgets)
  537. "Create mairix query from widget values WIDGETS."
  538. (let (query temp flag)
  539. ;; first we do the editable fields
  540. (dolist (cur mairix-widget-fields-list)
  541. ;; See if checkbox is checked
  542. (when (widget-value
  543. (cadr (assoc (concat "c" (car (cddr cur))) widgets)))
  544. ;; create query for the field
  545. (push
  546. (concat
  547. (nth 1 cur)
  548. ":"
  549. (mairix-replace-invalid-chars
  550. (widget-value
  551. (cadr (assoc (concat "e" (car (cddr cur))) widgets)))))
  552. query)))
  553. ;; Flags
  554. (when (member 'flags mairix-widget-other)
  555. (setq flag
  556. (mapconcat
  557. (function
  558. (lambda (flag)
  559. (setq temp
  560. (widget-value (cadr (assoc (car flag) mairix-widgets))))
  561. (if (string= "yes" temp)
  562. (cadr flag)
  563. (if (string= "no" temp)
  564. (concat "-" (cadr flag))))))
  565. '(("seen" "s") ("replied" "r") ("flagged" "f")) ""))
  566. (when (not (zerop (length flag)))
  567. (push (concat "F:" flag) query)))
  568. ;; return query string
  569. (mapconcat 'identity query " ")))
  570. (defun mairix-widget-create-query (&optional values)
  571. "Create widgets for creating mairix queries.
  572. Fill in VALUES if based on an article."
  573. (let (allwidgets)
  574. (when (get-buffer mairix-customize-query-buffer)
  575. (kill-buffer mairix-customize-query-buffer))
  576. (switch-to-buffer mairix-customize-query-buffer)
  577. (kill-all-local-variables)
  578. (erase-buffer)
  579. (widget-insert
  580. "Specify your query for Mairix using check boxes for activating fields.\n\n")
  581. (widget-insert
  582. (concat "Use ~word to match messages "
  583. (propertize "not" 'face 'italic)
  584. " containing the word)\n"
  585. " substring= to match words containing the substring\n"
  586. " substring=N to match words containing the substring, allowing\n"
  587. " up to N errors(missing/extra/different letters)\n"
  588. " ^substring= to match the substring at the beginning of a word.\n"))
  589. (widget-insert
  590. "Whitespace will be converted to ',' (i.e. AND). Use '/' for OR.\n\n")
  591. (setq mairix-widgets (mairix-widget-build-editable-fields values))
  592. (when (member 'flags mairix-widget-other)
  593. (widget-insert "\nFlags:\n Seen: ")
  594. (mairix-widget-add "seen"
  595. 'menu-choice
  596. :value "ignore"
  597. '(item "yes") '(item "no") '(item "ignore"))
  598. (widget-insert " Replied: ")
  599. (mairix-widget-add "replied"
  600. 'menu-choice
  601. :value "ignore"
  602. '(item "yes") '(item "no") '(item "ignore"))
  603. (widget-insert " Ticked: ")
  604. (mairix-widget-add "flagged"
  605. 'menu-choice
  606. :value "ignore"
  607. '(item "yes") '(item "no") '(item "ignore")))
  608. (when (member 'threads mairix-widget-other)
  609. (widget-insert "\n")
  610. (mairix-widget-add "Threads" 'checkbox nil))
  611. (widget-insert " Show full threads\n\n")))
  612. (defun mairix-widget-build-editable-fields (values)
  613. "Build editable field widgets in `nnmairix-widget-fields-list'.
  614. VALUES may contain values for editable fields from current article."
  615. (let ((ret))
  616. (mapc
  617. (function
  618. (lambda (field)
  619. (setq field (car (cddr field)))
  620. (setq
  621. ret
  622. (nconc
  623. (list
  624. (list
  625. (concat "c" field)
  626. (widget-create 'checkbox
  627. :tag field
  628. :notify (lambda (widget &rest ignore)
  629. (mairix-widget-toggle-activate widget))
  630. nil)))
  631. (list
  632. (list
  633. (concat "e" field)
  634. (widget-create 'editable-field
  635. :size 60
  636. :format (concat " " field ":"
  637. (make-string
  638. (- 11 (length field)) ?\ )
  639. "%v")
  640. :value (or (cadr (assoc field values)) ""))))
  641. ret))
  642. (widget-insert "\n")
  643. ;; Deactivate editable field
  644. (widget-apply (cadr (nth 1 ret)) :deactivate)))
  645. mairix-widget-fields-list)
  646. ret))
  647. (defun mairix-widget-add (name &rest args)
  648. "Add a widget NAME with optional ARGS."
  649. (push
  650. (list name
  651. (apply 'widget-create args))
  652. mairix-widgets))
  653. (defun mairix-widget-toggle-activate (widget)
  654. "Toggle activation status of WIDGET depending on checkbox value."
  655. (let ((field (widget-get widget :tag)))
  656. (if (widget-value widget)
  657. (widget-apply
  658. (cadr (assoc (concat "e" field) mairix-widgets))
  659. :activate)
  660. (widget-apply
  661. (cadr (assoc (concat "e" field) mairix-widgets))
  662. :deactivate)))
  663. (widget-setup))
  664. ;;;; Major mode for editing/deleting/saving searches
  665. (defvar mairix-searches-mode-map
  666. (let ((map (make-keymap)))
  667. (define-key map [(return)] 'mairix-select-search)
  668. (define-key map [(down)] 'mairix-next-search)
  669. (define-key map [(up)] 'mairix-previous-search)
  670. (define-key map [(right)] 'mairix-next-search)
  671. (define-key map [(left)] 'mairix-previous-search)
  672. (define-key map "\C-p" 'mairix-previous-search)
  673. (define-key map "\C-n" 'mairix-next-search)
  674. (define-key map [(q)] 'mairix-select-quit)
  675. (define-key map [(e)] 'mairix-select-edit)
  676. (define-key map [(d)] 'mairix-select-delete)
  677. (define-key map [(s)] 'mairix-select-save)
  678. map)
  679. "'mairix-searches-mode' keymap.")
  680. (defvar mairix-searches-mode-font-lock-keywords)
  681. (defun mairix-searches-mode ()
  682. "Major mode for editing mairix searches."
  683. (interactive)
  684. (kill-all-local-variables)
  685. (setq major-mode 'mairix-searches-mode)
  686. (setq mode-name "mairix-searches")
  687. (set-syntax-table text-mode-syntax-table)
  688. (use-local-map mairix-searches-mode-map)
  689. (make-local-variable 'font-lock-defaults)
  690. (setq mairix-searches-mode-font-lock-keywords
  691. (list (list "^\\([0-9]+\\)"
  692. '(1 font-lock-constant-face))
  693. (list "^[0-9 ]+\\(Name:\\) \\(.*\\)"
  694. '(1 font-lock-keyword-face) '(2 font-lock-string-face))
  695. (list "^[ ]+\\(Query:\\) \\(.*\\) , "
  696. '(1 font-lock-keyword-face) '(2 font-lock-string-face))
  697. (list ", \\(Threads:\\) \\(.*\\)"
  698. '(1 font-lock-keyword-face) '(2 font-lock-constant-face))
  699. (list "^\\([A-Z].*\\)$"
  700. '(1 font-lock-comment-face))
  701. (list "^[ ]+\\(Folder:\\) \\(.*\\)"
  702. '(1 font-lock-keyword-face) '(2 font-lock-string-face))))
  703. (setq font-lock-defaults '(mairix-searches-mode-font-lock-keywords)))
  704. (defun mairix-build-search-list ()
  705. "Display saved searches in current buffer."
  706. (insert "These are your current saved mairix searches.\n\
  707. You may use the following keys in this buffer: \n\
  708. Return: execute search, e: edit, d: delete, s: save, q: quit\n\
  709. Use cursor keys or C-n,C-p to select next/previous search.\n\n")
  710. (let ((num 0)
  711. (beg (point))
  712. current)
  713. (while (< num (length mairix-saved-searches))
  714. (setq current (nth num mairix-saved-searches))
  715. (setq num (1+ num))
  716. (mairix-insert-search-line num current)
  717. (insert "\n"))
  718. (goto-char beg)))
  719. (defun mairix-insert-search-line (number field)
  720. "Insert new mairix query with NUMBER and values FIELD in buffer."
  721. (insert
  722. (format "%d Name: %s\n Query: %s , Threads: %s\n Folder: %s\n"
  723. number
  724. (car field)
  725. (nth 1 field)
  726. (if (nth 3 field)
  727. "Yes"
  728. "No")
  729. (if (nth 2 field)
  730. (nth 2 field)
  731. "Default"))))
  732. (defun mairix-select-search ()
  733. "Call mairix with currently selected search."
  734. (interactive)
  735. (beginning-of-line)
  736. (if (not (looking-at "[0-9]+ Name"))
  737. (progn
  738. (ding)
  739. (message "Put cursor on a line with a search name first"))
  740. (progn
  741. (let* ((query (nth
  742. (1- (read (current-buffer)))
  743. mairix-saved-searches))
  744. (folder (nth 2 query)))
  745. (when (not folder)
  746. (setq folder mairix-search-file))
  747. (mairix-call-mairix
  748. (split-string (nth 1 query))
  749. folder
  750. (car (last query)))
  751. (mairix-select-quit)
  752. (mairix-show-folder folder)))))
  753. (defun mairix-next-search ()
  754. "Jump to next search."
  755. (interactive)
  756. (if (search-forward-regexp "^[0-9]+"
  757. (point-max)
  758. t
  759. 2)
  760. (beginning-of-line)
  761. (ding)))
  762. (defun mairix-previous-search ()
  763. "Jump to previous search."
  764. (interactive)
  765. (if (search-backward-regexp "^[0-9]+"
  766. (point-min)
  767. t)
  768. (beginning-of-line)
  769. (ding)))
  770. (defun mairix-select-quit ()
  771. "Quit mairix search mode."
  772. (interactive)
  773. (when mairix-searches-changed
  774. (mairix-select-save))
  775. (kill-buffer nil))
  776. (defun mairix-select-save ()
  777. "Save current mairix searches."
  778. (interactive)
  779. (when (y-or-n-p "Save mairix searches permanently in your .emacs? ")
  780. (customize-save-variable 'mairix-saved-searches mairix-saved-searches)))
  781. (defun mairix-select-edit ()
  782. "Edit currently selected mairix search."
  783. (interactive)
  784. (beginning-of-line)
  785. (if (not (looking-at "[0-9]+ Name"))
  786. (error "Put cursor on a line with a search name first")
  787. (progn
  788. (let* ((number (1- (read (current-buffer))))
  789. (query (nth number mairix-saved-searches))
  790. (folder (nth 2 query))
  791. newname newquery newfolder threads)
  792. (backward-char)
  793. (setq newname (read-string "Name of the search: " (car query)))
  794. (when (assoc newname (remq (nth number mairix-saved-searches)
  795. mairix-saved-searches))
  796. (error "This name does already exist"))
  797. (setq newquery (read-string "Query: " (nth 1 query)))
  798. (setq threads (y-or-n-p "Include whole threads? "))
  799. (setq newfolder
  800. (read-string "Mail folder (use empty string for default): "
  801. folder))
  802. (when (zerop (length newfolder))
  803. (setq newfolder nil))
  804. ;; set new values
  805. (setcar (nth number mairix-saved-searches) newname)
  806. (setcdr (nth number mairix-saved-searches)
  807. (list newquery newfolder threads))
  808. (setq mairix-searches-changed t)
  809. (let ((beg (point)))
  810. (forward-line 3)
  811. (end-of-line)
  812. (delete-region beg (point))
  813. (mairix-insert-search-line (1+ number)
  814. (nth number mairix-saved-searches))
  815. (goto-char beg))))))
  816. (defun mairix-select-delete ()
  817. "Delete currently selected mairix search."
  818. (interactive)
  819. (if (not (looking-at "[0-9]+ Name"))
  820. (error "Put cursor on a line with a search name first")
  821. (progn
  822. (let* ((number (1- (read (current-buffer))))
  823. (query (nth number mairix-saved-searches))
  824. beg)
  825. (backward-char)
  826. (when (y-or-n-p (format "Delete search %s ? " (car query)))
  827. (setq mairix-saved-searches
  828. (delq query mairix-saved-searches))
  829. (setq mairix-searches-changed t)
  830. (setq beg (point))
  831. (forward-line 4)
  832. (beginning-of-line)
  833. (delete-region beg (point))
  834. (while (search-forward-regexp "^[0-9]+"
  835. (point-max)
  836. t
  837. 1)
  838. (replace-match (number-to-string
  839. (setq number (1+ number)))))))
  840. (beginning-of-line))))
  841. (defun mairix-widget-get-values ()
  842. "Create values for editable fields from current article."
  843. (let ((get-mail-header
  844. (cadr (assq mairix-mail-program mairix-get-mail-header-functions))))
  845. (if get-mail-header
  846. (save-excursion
  847. (save-restriction
  848. (mapcar
  849. (function
  850. (lambda (field)
  851. (list (car (cddr field))
  852. (if (car field)
  853. (mairix-replace-invalid-chars
  854. (funcall get-mail-header (car field)))
  855. nil))))
  856. mairix-widget-fields-list)))
  857. (error "No function for obtaining mail header specified"))))
  858. (provide 'mairix)
  859. ;;; mairix.el ends here