Browse Source

org-contacts: import

Signed-off-by: Julien Danjou <julien@danjou.info>
Julien Danjou 9 years ago
parent
commit
c2d81e9b2d
2 changed files with 478 additions and 0 deletions
  1. 1 0
      contrib/README
  2. 477 0
      contrib/lisp/org-contacts.el

+ 1 - 0
contrib/README

@@ -16,6 +16,7 @@ org-bookmark.el          --- Links to bookmarks
 org-checklist.el         --- org functions for checklist handling
 org-choose.el            --- Use TODO keywords to mark decision states
 org-collector.el         --- Collect properties into tables
+org-contacts             --- Contacts management
 org-contribdir.el        --- Dummy file to mark the org contrib Lisp directory
 org-depend.el            --- TODO dependencies for Org-mode
 org-drill.el             --- Self-testing with org-learn

+ 477 - 0
contrib/lisp/org-contacts.el

@@ -0,0 +1,477 @@
+;;; org-contacts.el --- Contacts management
+
+;; Copyright (C) 2010, 2011 Julien Danjou <julien@danjou.info>
+
+;; Author: Julien Danjou <julien@danjou.info>
+;; Keywords: outlines, hypermedia, calendar
+;;
+;; This file is NOT part of GNU Emacs.
+;;
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;;; Commentary:
+
+;; This file contains the code for managing your contacts into Org-mode.
+
+;; To enter new contacts, you can use `org-capture' and a template just like
+;; this:
+
+;;         ("c" "Contacts" entry (file "~/Org/contacts.org")
+;;          "* %(org-contacts-template-name)
+;; :PROPERTIES:
+;; :EMAIL: %(org-contacts-template-email)
+;; :END:")))
+;;
+;;; Code:
+
+(eval-and-compile
+  (require 'org)
+  (require 'gnus)
+  (require 'gnus-art))
+
+(defgroup org-contacts nil
+  "Options concerning contacts management."
+  :group 'org)
+
+(defcustom org-contacts-files nil
+  "List of Org files to use as contacts source.
+If set to nil, all your Org files will be used."
+  :type '(repeat file)
+  :group 'org-contacts)
+
+(defcustom org-contacts-email-property "EMAIL"
+  "Name of the property for contact email address."
+  :type 'string
+  :group 'org-contacts)
+
+(defcustom org-contacts-birthday-property "BIRTHDAY"
+  "Name of the property for contact birthday date."
+  :type 'string
+  :group 'org-contacts)
+
+(defcustom org-contacts-last-read-mail-property "LAST_READ_MAIL"
+  "Name of the property for contact last read email link storage."
+  :type 'string
+  :group 'org-contacts)
+
+(defcustom org-contacts-icon-property "ICON"
+  "Name of the property for contact icon."
+  :type 'string
+  :group 'org-contacts)
+
+(defcustom org-contacts-nickname-property "NICKNAME"
+  "Name of the property for IRC nickname match."
+  :type 'string
+  :group 'org-contacts)
+
+(defcustom org-contacts-icon-size 32
+  "Size of the contacts icons."
+  :type 'string
+  :group 'org-contacts)
+
+(defcustom org-contacts-icon-use-gravatar (fboundp 'gravatar-retrieve)
+  "Whether use Gravatar to fetch contact icons."
+  :type 'boolean
+  :group 'org-contacts)
+
+(defcustom org-contacts-completion-ignore-case t
+  "Ignore case when completing contacts."
+  :type 'boolean
+  :group 'org-contacts)
+
+(defcustom org-contacts-group-prefix "+"
+  "Group prefix."
+  :type 'string
+  :group 'org-contacts)
+
+(defcustom org-contacts-matcher (concat org-contacts-email-property "<>\"\"")
+  "Matching rule for finding heading that are contacts.
+This can be a tag name, or a property check."
+  :type 'string
+  :group 'org-contacts)
+
+(defcustom org-contacts-email-link-description-format "%s (%d)"
+  "Format used to store links to email.
+This overrides `org-email-link-description-format' if set."
+  :group 'org-contacts
+  :type 'string)
+
+(defvar org-contacts-keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map "M" 'org-contacts-view-send-email)
+    (define-key map "i" 'org-contacts-view-switch-to-irc-buffer)
+    map)
+  "The keymap used in `org-contacts' result list.")
+
+(defun org-contacts-files ()
+  "Return list of Org files to use for contact management."
+  (or org-contacts-files (org-agenda-files t 'ifmode)))
+
+(defun org-contacts-filter (&optional name-match tags-match)
+  "Search for a contact maching NAME-MATCH and TAGS-MATCH.
+If both match values are nil, return all contacts."
+  (let ((tags-matcher
+         (if tags-match
+             (cdr (org-make-tags-matcher tags-match))
+           t))
+        (name-matcher
+         (if name-match
+             '(org-string-match-p name-match (org-get-heading t))
+           t))
+        (contacts-matcher
+         (cdr (org-make-tags-matcher org-contacts-matcher)))
+        markers result)
+    (dolist (file (org-contacts-files))
+      (org-check-agenda-file file)
+      (with-current-buffer (org-get-agenda-file-buffer file)
+        (unless (org-mode-p)
+          (error "File %s is no in `org-mode'" file))
+        (org-scan-tags
+         '(add-to-list 'markers (set-marker (make-marker) (point)))
+         `(and ,contacts-matcher ,tags-matcher ,name-matcher))))
+    (dolist (marker markers result)
+      (org-with-point-at marker
+        (add-to-list 'result
+                     (list (org-get-heading t) marker (org-entry-properties marker 'all)))))))
+
+(when (not (fboundp 'completion-table-case-fold))
+  ;; That function is new in Emacs 24...
+  (defun completion-table-case-fold (table string pred action)
+    (let ((completion-ignore-case t))
+      (complete-with-action action table string pred))))
+
+(defun org-contacts-complete-name (&optional start)
+  "Complete text at START with a user name and email."
+  (let* ((end (point))
+         (start (or start
+                    (save-excursion
+                      (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
+                      (goto-char (match-end 0))
+                      (point))))
+         (orig (buffer-substring start end))
+         (completion-ignore-case org-contacts-completion-ignore-case)
+         (group-completion-p (org-string-match-p (concat "^" org-contacts-group-prefix) orig))
+         (completion-list
+          (if group-completion-p
+              (mapcar (lambda (group) (propertize (concat org-contacts-group-prefix group) 'org-contacts-group group))
+                      (org-uniquify
+                       (loop for contact in (org-contacts-filter)
+                             with group-list
+                             nconc (org-split-string
+                                    (or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":"))))
+            (loop for contact in (org-contacts-filter)
+                  ;; The contact name is always the car of the assoc-list
+                  ;; returned by `org-contacts-filter'.
+                  for contact-name = (car contact)
+                  ;; Build the list of the user email addresses.
+                  for email-list = (split-string (or
+                                                  (cdr (assoc-string org-contacts-email-property (caddr contact)))
+                                                  ""))
+                  ;; If the user has email addresses…
+                  if email-list
+                  ;; … append a list of USER <EMAIL>.
+                  nconc (loop for email in email-list
+                              collect (org-contacts-format-email contact-name email)))))
+         (completion-list (all-completions orig completion-list)))
+    ;; If we are completing a group, and that's the only group, just return
+    ;; the real result.
+    (when (and group-completion-p
+               (= (length completion-list) 1))
+      (setq completion-list
+            (list (concat (car completion-list) ";: "
+                          (mapconcat 'identity
+                                     (loop for contact in (org-contacts-filter
+                                                           nil
+                                                           (get-text-property 0 'org-contacts-group (car completion-list)))
+                                           ;; The contact name is always the car of the assoc-list
+                                           ;; returned by `org-contacts-filter'.
+                                           for contact-name = (car contact)
+                                           ;; Grab the first email of the contact
+                                           for email = (car (split-string (or
+                                                                           (cdr (assoc-string org-contacts-email-property (caddr contact)))
+                                                                           "")))
+                                           ;; If the user has an email address, append USER <EMAIL>.
+                                           if email collect (org-contacts-format-email contact-name email))
+                                     ", ")))))
+    (list start end (if org-contacts-completion-ignore-case
+			(apply-partially #'completion-table-case-fold completion-list)
+		      completion-list))))
+
+(defun org-contacts-message-complete-function ()
+  "Function used in `completion-at-point-functions' in `message-mode'."
+  (let ((mail-abbrev-mode-regexp
+         "^\\(Resent-To\\|To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\|Disposition-Notification-To\\|Return-Receipt-To\\):"))
+        (when (mail-abbrev-in-expansion-header-p)
+          (org-contacts-complete-name))))
+
+(add-hook 'message-mode-hook
+          (lambda ()
+            (add-to-list 'completion-at-point-functions
+                         'org-contacts-message-complete-function)))
+
+(defun org-contacts-gnus-get-name-email ()
+  "Get name and email address from Gnus message."
+  (gnus-with-article-headers
+    (mail-extract-address-components
+     (or (mail-fetch-field "From") ""))))
+
+(defun org-contacts-gnus-article-from-get-marker ()
+  "Return a marker for a contact based on From."
+  (let* ((address (org-contacts-gnus-get-name-email))
+         (name (car address))
+         (email (cadr address)))
+    (cadar (or (org-contacts-filter
+                nil
+                (concat org-contacts-email-property "={\\b" (regexp-quote email) "\\b}"))
+               (when name
+                 (org-contacts-filter
+                  (concat "^" name "$")))))))
+
+(defun org-contacts-gnus-article-from-goto ()
+  "Go to contact in the From address of current Gnus message."
+  (interactive)
+  (let ((marker (org-contacts-gnus-article-from-get-marker)))
+    (when marker
+      (switch-to-buffer-other-window (marker-buffer marker))
+      (goto-char marker)
+      (when (org-mode-p)
+        (org-show-context 'agenda)
+        (save-excursion
+          (and (outline-next-heading)
+               ;; show the next heading
+               (org-flag-heading nil)))))))
+
+(define-key gnus-summary-mode-map ";" 'org-contacts-gnus-article-from-goto)
+
+(defun org-contacts-anniversaries (&optional field format)
+  "Compute FIELD anniversary for each contact, returning FORMAT.
+Default FIELD value is \"BIRTHDAY\".
+
+Format is a string matching the following format specification:
+
+  %h - Heading name
+  %l - Link to the heading
+  %y - Number of year
+  %Y - Number of year (ordinal)"
+  (let ((calendar-date-style 'american)
+        (entry ""))
+    (loop for contact in (org-contacts-filter)
+          for anniv = (let ((anniv (cdr (assoc-string
+                                         (or field org-contacts-birthday-property)
+                                         (caddr contact)))))
+                        (when anniv
+                          (calendar-gregorian-from-absolute
+                           (org-time-string-to-absolute anniv))))
+          ;; Use `diary-anniversary' to compute anniversary.
+          if (and anniv (apply 'diary-anniversary anniv))
+          collect (format-spec (or format "Birthday: %l (%Y)")
+                               `((?l . ,(org-with-point-at (cadr contact) (org-store-link nil)))
+                                 (?h . ,(car contact))
+                                 (?y . ,(- (calendar-extract-year date)
+                                           (calendar-extract-year anniv)))
+                                 (?Y . ,(let ((years (- (calendar-extract-year date)
+                                                        (calendar-extract-year anniv))))
+                                          (format "%d%s" years (diary-ordinal-suffix years)))))))))
+
+(defun org-completing-read-date (prompt collection
+                                        &optional predicate require-match initial-input
+                                        hist def inherit-input-method)
+  "Like `completing-read' but reads a date.
+Only PROMPT and DEF are really used."
+  (org-read-date nil nil nil prompt nil def))
+
+(add-to-list 'org-property-set-functions-alist
+             `(,org-contacts-birthday-property . org-completing-read-date))
+
+(defun org-contacts-template-name (&optional return-value)
+  "Try to return the contact name for a template.
+If not found return RETURN-VALUE or something that would ask the user."
+  (or (car (org-contacts-gnus-get-name-email))
+      return-value
+      "%^{Name}"))
+
+(defun org-contacts-template-email (&optional return-value)
+  "Try to return the contact email for a template.
+If not found return RETURN-VALUE or something that would ask the user."
+  (or (cadr (org-contacts-gnus-get-name-email))
+      return-value
+      (concat "%^{" org-contacts-email-property "}p")))
+
+(defun org-contacts-gnus-store-last-mail ()
+  "Store a link between mails and contacts.
+
+This function should be called from `gnus-article-prepare-hook'."
+  (let ((marker (org-contacts-gnus-article-from-get-marker)))
+    (when marker
+      (with-current-buffer (marker-buffer marker)
+        (save-excursion
+          (goto-char marker)
+          (let* ((org-email-link-description-format (or org-contacts-email-link-description-format
+                                                        org-email-link-description-format))
+                 (link (gnus-with-article-buffer (org-store-link nil))))
+            (org-set-property org-contacts-last-read-mail-property link)))))))
+
+(add-hook 'gnus-article-prepare-hook 'org-contacts-gnus-store-last-mail)
+
+(defun org-contacts-icon-as-string ()
+  (let ((image (org-contacts-get-icon)))
+    (concat
+     (propertize "-" 'display
+                 (append
+                  (if image
+                      image
+                    `'(space :width (,org-contacts-icon-size)))
+                  '(:ascent center)))
+     " ")))
+
+;;;###autoload
+(defun org-contacts (name)
+  "Create agenda view for contacts matching NAME."
+  (interactive (list (read-string "Name: ")))
+  (let ((org-agenda-files (org-contacts-files))
+        (org-agenda-skip-function
+         (lambda () (org-agenda-skip-if nil `(notregexp ,name))))
+        (org-agenda-format (propertize
+                            "%(org-contacts-icon-as-string)% p% s%(org-contacts-irc-number-of-unread-messages)%+T"
+                            'keymap org-contacts-keymap))
+        (org-agenda-overriding-header
+         (or org-agenda-overriding-header
+             (concat "List of contacts matching `" name "':"))))
+    (setq org-agenda-skip-regexp name)
+    (org-tags-view nil org-contacts-matcher)
+    (with-current-buffer org-agenda-buffer-name
+      (setq org-agenda-redo-command
+            (list 'org-contacts name)))))
+
+(defun org-contacts-completing-read (prompt
+                                     &optional predicate
+                                     initial-input hist def inherit-input-method)
+  "Call `completing-read' with contacts name as collection."
+  (org-completing-read
+   prompt (org-contacts-filter) predicate t initial-input hist def inherit-input-method))
+
+(defun org-contacts-format-email (name email)
+  "Format a mail address."
+  (unless email
+    (error "`email' cannot be nul"))
+  (if name
+      (concat name " <" email ">")
+    email))
+
+(defun org-contacts-check-mail-address (mail)
+  "Add MAIL address to contact at point if it does not have it."
+  (let ((mails (org-entry-get (point) org-contacts-email-property)))
+    (unless (member mail (split-string mails))
+      (when (yes-or-no-p
+             (format "Do you want to this address to %s?" (org-get-heading t)))
+        (org-set-property org-contacts-email-property (concat mails " " mail))))))
+
+(defun org-contacts-gnus-check-mail-address ()
+  "Check that contact has the current address recorded.
+This function should be called from `gnus-article-prepare-hook'."
+  (let ((marker (org-contacts-gnus-article-from-get-marker)))
+    (when marker
+      (org-with-point-at marker
+        (org-contacts-check-mail-address (cadr (org-contacts-gnus-get-name-email)))))))
+
+(add-hook 'gnus-article-prepare-hook 'org-contacts-gnus-check-mail-address)
+
+(defun org-contacts-view-send-email (&optional ask)
+  "Send email to the contact at point.
+If ASK is set, ask for the email address even if there's only one address."
+  (interactive "P")
+  (let ((marker (org-get-at-bol 'org-hd-marker)))
+    (org-with-point-at marker
+      (let ((emails (org-entry-get (point) org-contacts-email-property)))
+        (if emails
+            (let ((email-list (split-string emails)))
+              (if (and (= (length email-list) 1) (not ask))
+                  (compose-mail (org-contacts-format-email
+                                 (org-get-heading t) emails))
+                (let ((email (completing-read "Send mail to which address: " email-list)))
+                  (org-contacts-check-mail-address email)
+                  (compose-mail (org-contacts-format-email (org-get-heading t) email)))))
+          (error (format "This contact has no mail address set (no %s property)."
+                         org-contacts-email-property)))))))
+
+(defun org-contacts-get-icon (&optional pom)
+  "Get icon for contact at POM."
+  (setq pom (or pom (point)))
+  (catch 'icon
+    ;; Use `org-contacts-icon-property'
+    (let ((image-data (org-entry-get pom org-contacts-icon-property)))
+      (when image-data
+        (throw 'icon
+               (if (fboundp 'gnus-rescale-image)
+                   (gnus-rescale-image (create-image image-data)
+                                       (cons org-contacts-icon-size org-contacts-icon-size))
+                 (create-image image-data)))))
+    ;; Next, try Gravatar
+    (when org-contacts-icon-use-gravatar
+      (let* ((gravatar-size org-contacts-icon-size)
+             (email-list (org-entry-get pom org-contacts-email-property))
+             (gravatar
+              (when email-list
+                (loop for email in (split-string email-list)
+                      for gravatar = (gravatar-retrieve-synchronously email)
+                      if (and gravatar
+                              (not (eq gravatar 'error)))
+                      return gravatar))))
+        (when gravatar (throw 'icon gravatar))))))
+
+(defun org-contacts-irc-buffer (&optional pom)
+  "Get the IRC buffer associated with the entry at POM."
+  (setq pom (or pom (point)))
+  (let ((nick (org-entry-get pom org-contacts-nickname-property)))
+    (when nick
+      (let ((buffer (get-buffer nick)))
+        (when buffer
+          (with-current-buffer buffer
+            (when (eq major-mode 'erc-mode)
+              buffer)))))))
+
+(defun org-contacts-irc-number-of-unread-messages (&optional pom)
+  "Return the number of unread messages for contact at POM."
+  (when (boundp 'erc-modified-channels-alist)
+    (let ((number (cadr (assoc (org-contacts-irc-buffer pom) erc-modified-channels-alist))))
+      (if number
+          (format (concat "%3d unread message" (if (> number 1) "s" " ") " ") number)
+        (make-string 21 ? )))))
+
+(defun org-contacts-view-switch-to-irc-buffer ()
+  "Switch to the IRC buffer of the current contact if it has one."
+  (interactive)
+  (let ((marker (org-get-at-bol 'org-hd-marker)))
+    (org-with-point-at marker
+      (switch-to-buffer-other-window (org-contacts-irc-buffer)))))
+
+(defun org-contacts-completing-read-nickname (prompt collection
+                                                     &optional predicate require-match initial-input
+                                                     hist def inherit-input-method)
+  "Like `completing-read' but reads a nickname."
+  (org-completing-read prompt (append collection (erc-nicknames-list)) predicate require-match
+                       initial-input hist def inherit-input-method))
+
+(defun erc-nicknames-list ()
+  "Return all nicknames of all ERC buffers."
+  (loop for buffer in (erc-buffer-list)
+        nconc (with-current-buffer buffer
+                (loop for user-entry in (mapcar 'car (erc-get-channel-user-list))
+                      collect (elt user-entry 1)))))
+
+(add-to-list 'org-property-set-functions-alist
+             `(,org-contacts-nickname-property . org-contacts-completing-read-nickname))
+
+(provide 'org-contacts)