;;; 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-when-compile
  (require 'cl))

(eval-and-compile
  (require 'org))

(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-address-property "ADDRESS"
  "Name of the property for contact 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-birthday-format "Birthday: %l (%Y)"
  "Format of the anniversary agenda entry. The following replacements are available:

  %h - Heading name
  %l - Link to the heading
  %y - Number of year
  %Y - Number of year (ordinal)"
  :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)

(defcustom org-contacts-vcard-file "contacts.vcf"
  "Default file for vcard export."
  :group 'org-contacts
  :type 'file)

(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))))

(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)))))))

(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 ""))
    (unless format (setq format org-contacts-birthday-format))
    (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 format
                               `((?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)))))))

(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)))))))

(defun org-contacts-gnus-insinuate ()
  "Add some hooks for Gnus user.
This adds `org-contacts-gnus-check-mail-address' and
`org-contacts-gnus-store-last-mail' to
`gnus-article-prepare-hook'. It also adds a binding on `;' in
`gnus-summary-mode-map' to `org-contacts-gnus-article-from-goto'"
  (require 'gnus)
  (require 'gnus-art)
  (define-key gnus-summary-mode-map ";" 'org-contacts-gnus-article-from-goto)
  (add-hook 'gnus-article-prepare-hook 'org-contacts-gnus-check-mail-address)
  (add-hook 'gnus-article-prepare-hook 'org-contacts-gnus-store-last-mail))

(when (boundp 'completion-at-point-functions)
  (add-hook 'message-mode-hook
	    (lambda ()
	      (add-to-list 'completion-at-point-functions
			   'org-contacts-message-complete-function))))

(defun org-contacts-wl-get-from-header-content ()
  "Retrieve the content of the `From' header of an email.
Works from wl-summary-mode and mime-view-mode - that is while viewing email.
Depends on Wanderlust been loaded."
  (with-current-buffer (org-capture-get :original-buffer)
    (cond
     ((eq major-mode 'wl-summary-mode) (when wl-summary-buffer-elmo-folder
                                         (elmo-message-field
                                          wl-summary-buffer-elmo-folder
                                          (wl-summary-message-number)
                                          'from)))
     ((eq major-mode 'mime-view-mode) (std11-narrow-to-header)
                                      (prog1
                                          (std11-fetch-field "From")
                                        (widen))))))

(defun org-contacts-wl-get-name-email ()
  "Get name and email address from wanderlust email.
See `org-contacts-wl-get-from-header-content' for limitations."
  (let ((from (org-contacts-wl-get-from-header-content)))
    (when from
      (list (wl-address-header-extract-realname from)
	    (wl-address-header-extract-address from)))))

(defun org-contacts-template-wl-name (&optional return-value)
  "Try to return the contact name for a template from wl.
If not found return RETURN-VALUE or something that would ask the user."
  (or (car (org-contacts-wl-get-name-email))
      return-value
      "%^{Name}"))

(defun org-contacts-template-wl-email (&optional return-value)
  "Try to return the contact email for a template from wl.
If not found return RETURN-VALUE or something that would ask the user."
  (or (cadr (org-contacts-wl-get-name-email))
      return-value
      (concat "%^{" org-contacts-email-property "}p")))

(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))

(defun org-contacts-vcard-escape (str)
  "Escape ; , and \n in STR for use in the VCard format.
Thanks to http://www.emacswiki.org/cgi-bin/wiki/bbdb-vcard-export.el for the regexp."
  (when str
    (replace-regexp-in-string "\n" "\\\\n" (replace-regexp-in-string "\\(;\\|,\\|\\\\\\)" "\\\\\\1" str))))

(defun org-contacts-vcard-encode-name (name)
  "Try to encode NAME as VCard's N property. The N property expects FamilyName;GivenName;AdditionalNames;Prefix;Postfix.
Org-contacts does not specify how to encode the name. So we try to do our best."
  (concat (replace-regexp-in-string "\\(\\w+\\) \\(.*\\)" "\\2;\\1" name) ";;;"))

(defun org-contacts-vcard-format (contact)
  "Formats CONTACT in VCard 3.0 format."
  (let* ((properties (caddr contact))
	 (name (org-contacts-vcard-escape (car contact)))
	 (n (org-contacts-vcard-encode-name name))
	 (email (org-contacts-vcard-escape (cdr (assoc-string org-contacts-email-property properties))))
	 (bday (org-contacts-vcard-escape (cdr (assoc-string org-contacts-birthday-property properties))))
	 (addr (cdr (assoc-string org-contacts-address-property properties)))
	 (nick (org-contacts-vcard-escape (cdr (assoc-string org-contacts-nickname-property properties))))

	 (head (format "BEGIN:VCARD\nVERSION:3.0\nN:%s\nFN:%s\n" n name)))
    (concat head
	    (when email (format "EMAIL:%s\n" email))
	    (when addr
	      (format "ADR:;;%s\n" (replace-regexp-in-string "\\, ?" ";" addr)))
	    (when bday
	      (let ((cal-bday (calendar-gregorian-from-absolute (org-time-string-to-absolute bday))))
		(format "BDAY:%04d-%02d-%02d\n"
			(calendar-extract-year cal-bday)
			(calendar-extract-month cal-bday)
			(calendar-extract-day cal-bday))))
	    (when nick (format "NICKNAME:%s\n" nick))
	    "END:VCARD\n\n")))

(defun org-contacts-export-as-vcard (&optional name file to-buffer)
  "Export all contacts matching NAME as VCard 3.0. It TO-BUFFER is nil, the content is written to FILE or `org-contacts-vcard-file'. If TO-BUFFER is non-nil, the buffer is created and the VCard is written into that buffer."
  (interactive) ; TODO ask for name?
  (let* ((filename (or file org-contacts-vcard-file))
	 (buffer (if to-buffer
		     (get-buffer-create to-buffer)
		     (find-file-noselect filename))))

    (message "Exporting...")

    (set-buffer buffer)
    (let ((inhibit-read-only t)) (erase-buffer))
    (fundamental-mode)
    (org-install-letbind)

    (when (fboundp 'set-buffer-file-coding-system)
      (set-buffer-file-coding-system coding-system-for-write))

    (loop for contact in (org-contacts-filter name)
	 do (insert (org-contacts-vcard-format contact)))

    (if to-buffer
	(current-buffer)
	(progn (save-buffer) (kill-buffer)))))

(defun org-contacts-show-map (&optional name)
  "Show contacts on a map. Requires google-maps-el."
  (interactive)
  (unless (fboundp 'google-maps-static-show)
    (error "org-contacts-show-map requires google-maps-el."))
  (google-maps-static-show
   :markers
   (loop
      for contact in (org-contacts-filter name)
      for addr = (cdr (assoc-string org-contacts-address-property (caddr contact)))
      if addr
      collect (cons (list addr) (list :label (string-to-char (car contact)))))))

(provide 'org-contacts)
