├── .github └── issue_template.md ├── .gitignore ├── README.md └── helm-bbdb.el /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ## Expected behavior 2 | 3 | 4 | ## Actual behavior from emacs-helm.sh if possible 5 | 6 | 7 | ## Steps to reproduce (recipe) 8 | 9 | 10 | ## Backtraces if some (M-x toggle-debug-on-error) 11 | 12 | 13 | ## Describe versions of helm, emacs, operating system etc. 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MELPA](http://melpa.milkbox.net/packages/helm-bbdb-badge.svg)](http://melpa.milkbox.net/#/helm-bbdb) 2 | [![MELPA Stable](https://stable.melpa.org/packages/helm-bbdb-badge.svg)](https://stable.melpa.org/#/helm-bbdb) 3 | 4 | # helm-bbdb 5 | 6 | A Helm interface for BBDB, the Insidious Big Brother Database for GNU Emacs. 7 | 8 | # Features 9 | 10 | * List all contacts in the bbdb database. 11 | * Match name, email or organization. 12 | * Send email to one or more contacts (marked). 13 | * Display one or more contacts in the bbdb buffer (marked). 14 | * Record new contacts. 15 | * Delete one or more contacts (marked). 16 | * Support auto-completion in message-mode buffers. 17 | 18 | # Dependencies 19 | 20 | [helm](https://github.com/emacs-helm/helm) and [bbdb](http://melpa.milkbox.net/#/bbdb) 21 | 22 | # Install 23 | 24 | ## From source 25 | 26 | ```elisp 27 | (add-to-list 'load-path "/path/to/helm-bbdb") 28 | (autoload 'helm-bbdb "helm-bbdb.el" nil t) 29 | ``` 30 | 31 | ## From melpa 32 | 33 | Just install from Melpa and once `(package-initialize)` loads and activates the package, `helm-bbdb` should be available. 34 | 35 | # Configuration 36 | 37 | To use address auto-completion in message-mode buffers with TAB, add `helm-bbdb-expand-name` to the `message-completion-alist` variable. 38 | 39 | # Bugs and other issues 40 | 41 | The `bbdb-edit-field` command may not work well with helm mode. This is because `bbdb-edit-address-street` needs an empty string to break the loop. However, there's no way of inputting an empty string in Helm if `bbdb-street-list` is not empty, so you may have to disable helm completion for `bbdb-edit-field`, and possibly `bbdb-insert-field` as well: 42 | 43 | ```elisp 44 | (add-to-list 'helm-completing-read-handlers-alist '(bbdb-edit-field . nil)) 45 | ``` 46 | 47 | # Related project 48 | 49 | [Addressbook bookmark](https://github.com/thierryvolpiatto/addressbook-bookmark) is a contact manager for emacs similar to `bbdb` but much lighter (only one file `addressbook-bookmark.el`) and without all the `bbdb` features you will never use. It provides completion in message-mode buffers using the helm interface, which is how helm works out of the box with M-x `helm-addressbook-bookmarks`. Contacts are stored in emacs bookmark file, which means the database format is much simpler and lighter than `bbdb`'s database. 50 | -------------------------------------------------------------------------------- /helm-bbdb.el: -------------------------------------------------------------------------------- 1 | ;;; helm-bbdb.el --- Helm interface for bbdb -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2012 ~ 2019 Thierry Volpiatto 4 | 5 | ;; Version: 1.0 6 | ;; Package-Requires: ((emacs "24.3") (helm "1.5") (bbdb "3.1.2")) 7 | ;; URL: https://github.com/emacs-helm/helm-bbdb 8 | 9 | ;; This program is free software; you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; This program is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with this program. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; This is a Helm interface for BBDB, the Insidious Big Brother 25 | ;; Database for GNU Emacs. 26 | 27 | ;;; Code: 28 | 29 | (require 'cl-lib) 30 | (require 'helm) 31 | (require 'helm-utils) 32 | (require 'helm-mode) 33 | 34 | (defvar bbdb-records) 35 | (defvar bbdb-buffer-name) 36 | (defvar bbdb-phone-label-list) 37 | (defvar bbdb-address-label-list) 38 | (defvar bbdb-default-xfield) 39 | 40 | (declare-function bbdb "ext:bbdb-com") 41 | (declare-function bbdb-current-record "ext:bbdb-com") 42 | (declare-function bbdb-redisplay-record "ext:bbdb-com") 43 | (declare-function bbdb-record-mail "ext:bbdb-com" (record) t) 44 | (declare-function bbdb-mail-address "ext:bbdb-com") 45 | (declare-function bbdb-records "ext:bbdb-com") 46 | (declare-function bbdb-create-internal "ext:bbdb-com") 47 | (declare-function bbdb-read-organization "ext:bbdb-com") 48 | (declare-function bbdb-read-xfield "ext:bbdb-com") 49 | (declare-function bbdb-display-records "ext:bbdb") 50 | (declare-function bbdb-current-field "ext:bbdb") 51 | (declare-function bbdb-delete-field-or-record "ext:bbdb-com") 52 | (declare-function bbdb-record-organization "ext:bbdb") 53 | (declare-function bbdb-record-name "ext:bbdb") 54 | 55 | (defvar helm-bbdb--cache nil) 56 | 57 | (defgroup helm-bbdb nil 58 | "Commands and functions for bbdb." 59 | :group 'helm) 60 | 61 | (defcustom helm-bbdb-actions 62 | (helm-make-actions 63 | "View contact's data" 'helm-bbdb-view-person-action 64 | "Delete contact" 'helm-bbdb-delete-contact 65 | "Send an email" 'helm-bbdb-compose-mail) 66 | "Default actions alist for `helm-source-bbdb'." 67 | :type '(alist :key-type string :value-type function)) 68 | 69 | (defun helm-bbdb-candidates () 70 | "Return a list of all names in the bbdb database." 71 | (cl-loop for bbdb-record in (bbdb-records) 72 | for name = (bbdb-record-name bbdb-record) 73 | collect (cons name bbdb-record))) 74 | 75 | (defun helm-bbdb-read-phone () 76 | "Return a list of vector address objects. 77 | See docstring of `bbdb-create-internal' for more info on address entries." 78 | (cl-loop with loc-list = (cons "[Exit when no more]" bbdb-phone-label-list) 79 | with loc ; Defer count 80 | do (setq loc (helm-comp-read (format "Phone location[%s]: " count) 81 | loc-list 82 | :must-match 'confirm 83 | :default "")) 84 | while (not (string= loc "[Exit when no more]")) 85 | for count from 1 86 | for phone-number = (helm-read-string (format "Phone number (%s): " loc)) 87 | collect (vector loc phone-number) into phone-list 88 | do (setq loc-list (remove loc loc-list)) 89 | finally return phone-list)) 90 | 91 | (defun helm-bbdb-read-address () 92 | "Return a list of vector address objects. 93 | See docstring of `bbdb-create-internal' for more info on address entries." 94 | (cl-loop with loc-list = (cons "[Exit when no more]" bbdb-address-label-list) 95 | with loc ; Defer count 96 | do (setq loc (helm-comp-read 97 | (format "Address description[%s]: " 98 | (int-to-string count)) 99 | loc-list 100 | :must-match 'confirm 101 | :default "")) 102 | while (not (string= loc "[Exit when no more]")) 103 | for count from 1 104 | ;; Create vector 105 | for lines = (helm-read-repeat-string "Street, line" t) 106 | for city = (helm-read-string "City: ") 107 | for state = (helm-read-string "State: ") 108 | for zip = (helm-read-string "ZipCode: ") 109 | for country = (helm-read-string "Country: ") 110 | collect (vector loc lines city state zip country) into address-list 111 | do (setq loc-list (remove loc loc-list)) 112 | finally return address-list)) 113 | 114 | (defun helm-bbdb-create-contact (actions candidate) 115 | "Action transformer for `helm-source-bbdb'. 116 | Returns only an entry to add the current `helm-pattern' as new contact. 117 | All other actions are removed." 118 | (require 'bbdb-com) 119 | (if (and (stringp candidate) 120 | (string= candidate "*Add new contact*")) 121 | (helm-make-actions 122 | "Add to contacts" 123 | (lambda (_actions) 124 | (bbdb-create-internal 125 | :name (read-from-minibuffer "Name: " helm-pattern) 126 | :organization (bbdb-read-organization) 127 | :mail (helm-read-repeat-string "Email " t) 128 | :phone (helm-bbdb-read-phone) 129 | :address (helm-bbdb-read-address) 130 | :xfields (let ((xfield (bbdb-read-xfield bbdb-default-xfield))) 131 | (unless (string= xfield "") 132 | (list (cons bbdb-default-xfield xfield))))))) 133 | actions)) 134 | 135 | (defun helm-bbdb-get-record (candidate) 136 | "Return record that match CANDIDATE." 137 | (cl-letf (((symbol-function 'message) #'ignore)) 138 | (bbdb candidate nil) 139 | (set-buffer bbdb-buffer-name) 140 | (bbdb-current-record))) 141 | 142 | (defun helm-bbdb-match-mail (candidate) 143 | "Additional match function for matching the CANDIDATE's email address." 144 | (string-match helm-pattern 145 | (mapconcat 146 | 'identity 147 | (bbdb-record-mail 148 | (assoc-default candidate helm-bbdb--cache)) 149 | ","))) 150 | 151 | (defun helm-bbdb-match-org (candidate) 152 | "Additional match function for matching the CANDIDATE's organization." 153 | (string-match helm-pattern 154 | (mapconcat 155 | 'identity 156 | (bbdb-record-organization 157 | (assoc-default candidate helm-bbdb--cache)) 158 | ","))) 159 | 160 | (defvar helm-source-bbdb 161 | (helm-build-sync-source "BBDB" 162 | :init (lambda () 163 | (require 'bbdb) 164 | (setq helm-bbdb--cache (helm-bbdb-candidates))) 165 | :candidates 'helm-bbdb--cache 166 | :match '(helm-bbdb-match-mail helm-bbdb-match-org) 167 | :action 'helm-bbdb-actions 168 | :persistent-action 'helm-bbdb-persistent-action 169 | :persistent-help "View data" 170 | :filtered-candidate-transformer (lambda (candidates _source) 171 | (if (not candidates) 172 | (list "*Add new contact*") 173 | candidates)) 174 | :action-transformer (lambda (actions candidate) 175 | (helm-bbdb-create-contact actions candidate))) 176 | "Source for BBDB.") 177 | 178 | (defun helm-bbdb--view-person-action-1 (candidates) 179 | (bbdb-display-records 180 | (mapcar 'helm-bbdb-get-record candidates) nil t)) 181 | 182 | (defun helm-bbdb--marked-contacts () 183 | (cl-loop for record in (helm-marked-candidates) 184 | for name = (bbdb-record-name record) 185 | collect 186 | name)) 187 | 188 | (defun helm-bbdb-view-person-action (_candidate) 189 | "View BBDB data of single CANDIDATE or marked candidates." 190 | (helm-bbdb--view-person-action-1 (helm-bbdb--marked-contacts))) 191 | 192 | (defun helm-bbdb-persistent-action (candidate) 193 | "Persistent action to view CANDIDATE's data." 194 | (let ((bbdb-silent t)) 195 | (helm-bbdb-view-person-action candidate))) 196 | 197 | (defun helm-bbdb-collect-mail-addresses () 198 | "Return a list of the mail addresses of candidates. 199 | If record has more than one address, prompt for an address." 200 | (cl-loop for record in (helm-marked-candidates) 201 | for mail = (bbdb-record-mail record) 202 | when mail collect 203 | (if (cdr mail) 204 | (helm-comp-read "Choose mail: " 205 | (mapcar (lambda (mail) 206 | (bbdb-dwim-mail record mail)) 207 | mail) 208 | :allow-nest t 209 | :initial-input helm-pattern) 210 | (bbdb-dwim-mail record (car mail))))) 211 | 212 | (defun helm-bbdb-collect-all-mail-addresses () 213 | "Return a list of strings to use as the mail address of record. 214 | This may include multiple addresses of the same record. The name in 215 | the mail address is formatted obeying `bbdb-mail-name-format' and 216 | `bbdb-mail-name'." 217 | (let (mails) 218 | (dolist (record (bbdb-records)) 219 | (let ((mail (bbdb-record-mail record))) 220 | (when mail 221 | (if (> (length mail) 1) 222 | ;; The idea here is to keep adding to the list however 223 | ;; many addresses are found in the record. 224 | (let ((addresses mail)) 225 | (mapc (lambda (mail) 226 | (push (bbdb-dwim-mail record mail) mails)) 227 | addresses)) 228 | (let ((mail (bbdb-dwim-mail record (car mail)))) 229 | (push mail mails)))))) 230 | (mapc #'identity mails))) 231 | 232 | (defun helm-bbdb-compose-mail (_candidate) 233 | "Compose a new mail to one or multiple CANDIDATEs." 234 | (let* ((address-list (helm-bbdb-collect-mail-addresses)) 235 | (address-str (mapconcat 'identity address-list ",\n "))) 236 | (compose-mail address-str nil nil nil 'switch-to-buffer))) 237 | 238 | (defun helm-bbdb-delete-contact (_candidate) 239 | "Delete CANDIDATE from the bbdb buffer and database. 240 | Prompt user to confirm deletion." 241 | (let ((cands (helm-bbdb--marked-contacts))) 242 | (with-helm-display-marked-candidates 243 | "*bbdb candidates*" cands 244 | (when (y-or-n-p "Delete contacts") 245 | (helm-bbdb-view-person-action 'ignore) 246 | (delete-window) 247 | (with-current-buffer bbdb-buffer-name 248 | (helm-awhile (let ((field (ignore-errors (bbdb-current-field))) 249 | (record (ignore-errors (bbdb-current-record)))) 250 | (and field record (list record field t))) 251 | (apply 'bbdb-delete-field-or-record it)) 252 | (message "%s contacts deleted: \n- %s" 253 | (length cands) 254 | (mapconcat 'identity cands "\n- "))))))) 255 | 256 | (defun helm-bbdb-insert-mail (_candidate &optional comma) 257 | "Insert CANDIDATE's email address. 258 | If optional argument COMMA is non-nil, insert comma separator as well, 259 | which is needed when executing persistent action." 260 | (let* ((address-list (cl-loop for candidate in (helm-marked-candidates) 261 | collect candidate)) 262 | (address-str (mapconcat 'identity address-list ",\n "))) 263 | (end-of-line) 264 | (while (not (looking-back ": \\|, \\| [ \t]" (point-at-bol))) 265 | (delete-char -1)) 266 | (insert (concat address-str (when comma ", "))) 267 | (end-of-line))) 268 | 269 | (defun helm-bbdb-expand-name () 270 | "Expand name under point when there is one. 271 | Otherwise, open a helm buffer displaying a list of addresses. If 272 | `bbdb-complete-mail-allow-cycling' is non-nil and point is at the end 273 | of the address line, cycle mail addresses of record. 274 | 275 | To use this feature, make sure `helm-bbdb-expand-name' is added to the 276 | `message-completion-alist' variable." 277 | (if (and (looking-back "\\(<.+\\)\\(@\\)\\(.+>$\\)" nil) 278 | bbdb-complete-mail-allow-cycling) 279 | (bbdb-complete-mail) 280 | (let (mails 281 | (abbrev (thing-at-point 'symbol t))) 282 | (with-temp-buffer (mapc (lambda (mail) 283 | (insert (concat mail "\n"))) 284 | (helm-bbdb-collect-all-mail-addresses)) 285 | (goto-char (point-min)) 286 | (while (re-search-forward (concat "\\(^.+\\)" "\\(" abbrev "\\)" "\\(.+$\\)") nil t) 287 | (push (concat (match-string 1) (match-string 2) (match-string 3)) mails) 288 | (setq mails mails))) 289 | ;; If there's one address, insert it automatically 290 | (if (= (length mails) 1) 291 | (progn (end-of-line) 292 | (while (not (looking-back ": \\|, \\| [ \t]" (point-at-bol))) 293 | (delete-char -1)) 294 | (insert (car mails)) 295 | (end-of-line)) 296 | ;; If there's more than one, start helm 297 | (helm :sources (helm-build-sync-source "BBDB" 298 | :candidates 'helm-bbdb-collect-all-mail-addresses 299 | :persistent-action (lambda (candidate) 300 | (helm-bbdb-insert-mail candidate t)) 301 | :action 'helm-bbdb-insert-mail) 302 | :input (thing-at-point 'symbol t)))))) 303 | 304 | ;;;###autoload 305 | (defun helm-bbdb () 306 | "Preconfigured `helm' for BBDB. 307 | 308 | Needs BBDB. 309 | 310 | URL `http://bbdb.sourceforge.net/'" 311 | (interactive) 312 | (helm-other-buffer 'helm-source-bbdb "*helm bbdb*")) 313 | 314 | (provide 'helm-bbdb) 315 | 316 | ;; Local Variables: 317 | ;; byte-compile-warnings: (not obsolete) 318 | ;; coding: utf-8 319 | ;; indent-tabs-mode: nil 320 | ;; End: 321 | 322 | ;;; helm-bbdb.el ends here 323 | --------------------------------------------------------------------------------