From a7124c7723e71ead527d7796c474c8b3bb19ed29 Mon Sep 17 00:00:00 2001 From: Sacha Chua Date: Wed, 19 Oct 2022 18:39:54 -0400 Subject: Add mail merge --- emacsconf-mail.el | 206 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 emacsconf-mail.el diff --git a/emacsconf-mail.el b/emacsconf-mail.el new file mode 100644 index 0000000..c5ec599 --- /dev/null +++ b/emacsconf-mail.el @@ -0,0 +1,206 @@ +;;; emacsconf-mail.el --- Mail merge functions -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 Sacha Chua + +;; Author: Sacha Chua +;; Keywords: mail + +;; This program 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. + +;; This program 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 this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(defun emacsconf-mail-complete-email-group (&optional info) + "Return (email . (talk talk))." + (setq info (emacsconf-filter-talks (or info (emacsconf-get-talk-info)))) + (save-window-excursion + (let* ((grouped (seq-group-by (lambda (o) (plist-get o :email)) info)) + (slug (emacsconf-get-slug-from-string (emacsconf-complete-talk))) + (email (plist-get (seq-find (lambda (o) (string= (plist-get o :slug) slug)) info) :email))) + (assoc email grouped)))) + +(defun emacsconf-mail-prepare (template group attrs) + (compose-mail + (car group) + (emacsconf-replace-plist-in-string attrs (plist-get template :subject)) + `(("Reply-To" . ,(emacsconf-replace-plist-in-string attrs (plist-get template :reply-to))) + ("Mail-Followup-To" . ,(emacsconf-replace-plist-in-string attrs (plist-get template :mail-followup-to))) + ("Cc" . ,(plist-get template :cc)))) + (message-sort-headers) + (message-goto-body) + (save-excursion (insert (emacsconf-replace-plist-in-string attrs (plist-get template :body))) + (goto-char (point-min)) + (emacsconf-mail-merge-wrap))) + +(defun emacsconf-mail-template-to-me () + "Might be useful for testing." + (interactive) + (let* ((template (if (org-entry-get (point) "EMAIL_ID") + (emacsconf-mail-merge-get-template-from-subtree) + (emacsconf-mail-merge-get-template + (completing-read "Template: " (org-property-values "EMAIL_ID"))))) + (mail-func (plist-get template :function)) + (group (emacsconf-mail-complete-email-group))) + (funcall mail-func (cons user-mail-address (cdr group)) template))) + +(defun emacsconf-mail-template-to-group () + "Prompt for a speaker and e-mail current template to them." + (interactive) + (let* ((template (if (org-entry-get (point) "EMAIL_ID") + (emacsconf-mail-merge-get-template-from-subtree) + (emacsconf-mail-merge-get-template + (completing-read "Template: " (org-property-values "EMAIL_ID"))))) + (mail-func (plist-get template :function))) + (funcall mail-func (emacsconf-mail-complete-email-group) template))) + +(defun emacsconf-mail-template-to-all () + "Uses the current template to draft messages to all the speakers." + (interactive) + (let* ((template (if (org-entry-get (point) "EMAIL_ID") + (emacsconf-mail-merge-get-template-from-subtree) + (emacsconf-mail-merge-get-template + (completing-read "Template: " (org-property-values "EMAIL_ID"))))) + (info (seq-filter (lambda (o) + (if (plist-get template :slugs) + (member (plist-get o :slug) + (split-string (plist-get template :slugs) " ")) + t)) + (emacsconf-filter-talks (emacsconf-get-talk-info)))) + (grouped (emacsconf-mail-group-by-email info)) + (mail-func (plist-get template :function))) + (mapc (lambda (group) + (funcall mail-func group template)) + grouped))) + +(defun emacsconf-mail-group-by-email (info) + (seq-group-by (lambda (o) (plist-get o :email)) info)) + +(defun emacsconf-mail-speaker (&optional subject body) + "Compose a message to the speaker of the current talk." + (interactive) + (compose-mail (format "%s <%s>" (org-entry-get (point) "NAME") (org-entry-get (point) "EMAIL")) subject) + (when body (message-goto-body) (insert body))) + +(defun emacsconf-mail-speaker-schedule (&optional subject body) + (interactive (list (read-string "Subject: ") nil)) + (let ((info (emacsconf-get-talk-info-for-subtree))) + (emacsconf-mail-speaker subject body) + (when body (message-goto-body) (insert body)) + (goto-char (point-max)) + (insert (string-join (emacsconf-timezone-strings info) "\n")))) + +(defvar emacsconf-submit-email "emacsconf-submit@gnu.org" "E-mail address for submissions.") + +(defun emacsconf-mail-speaker-cc-submit (&optional subject body) + "Compose a message to the speaker of the current talk." + (interactive) + (compose-mail (format "%s <%s>" (org-entry-get (point) "NAME") (org-entry-get (point) "EMAIL")) + subject '(("Reply-To" . emacsconf-submit-email) ("Cc" . emacsconf-submit-email))) + (message-goto-body) + (when body (insert body)) + (save-excursion (insert "Please keep " emacsconf-submit-email " in the To: or Cc: when replying. Thank you!"))) + +(defun emacsconf-mail-show-talk-info () + (interactive) + (let ((email (or (mail-fetch-field "reply-to") (mail-fetch-field "from")))) + (when (string-match "<\\(\\(\\sw\\|\\s_\\|\\s.\\)+@\\(\\sw\\|\\s_\\|\\s.\\)+\\)>" email) + (setq email (match-string 1 email))) + (pop-to-buffer (find-file-noselect emacsconf-org-file)) + (goto-char (point-min)) + (goto-char + (or (org-find-property "EMAIL" email) + (org-find-property "NAME" + (completing-read "Name: " (delq nil (org-map-entries (lambda () (org-entry-get "NAME")))))))))) + + + + + +(defun emacsconf-mail-merge-wrap () + (interactive) + (with-undo-amalgamate + (save-excursion + (while (re-search-forward " *${wrap}" nil t) + (replace-match "") + (fill-paragraph))))) + +(defun emacsconf-mail-merge-get-template-from-subtree () + (list :subject (org-entry-get-with-inheritance "SUBJECT") + :cc (org-entry-get-with-inheritance "CC") + :slugs (org-entry-get-with-inheritance "SLUGS") + :reply-to (or (org-entry-get-with-inheritance "REPLY_TO") (org-entry-get-with-inheritance "REPLY-TO")) + :mail-followup-to (or (org-entry-get-with-inheritance "MAIL_FOLLOWUP_TO") + (org-entry-get-with-inheritance "MAIL-FOLLOWUP-TO")) + :body (replace-regexp-in-string "\n *," "\n" (buffer-substring-no-properties + (progn (org-end-of-meta-data) (point)) + (org-end-of-subtree))) + :function (when (org-entry-get-with-inheritance "FUNCTION") + (intern (org-entry-get-with-inheritance "FUNCTION"))))) + +(defun emacsconf-mail-merge-get-template (id) + "Return the information for the e-mail template with EMAIL_ID set to ID." + (save-excursion + (goto-char (org-find-property "EMAIL_ID" id)) + (emacsconf-mail-merge-get-template-from-subtree))) + +(defun emacsconf-mail-merge-fill (string) + "Fill in the values for STRING using the properties at point. +Include some other things, too, such as emacsconf-year, title, name, email, url, and duration." + (let (start (values `(("year" . ,emacsconf-year) + ("title" . ,(org-entry-get (point) "ITEM")) + ("name" . ,(org-entry-get (point) "NAME")) + ("email" . ,(org-entry-get (point) "EMAIL")) + ("url" . ,(format "%s%s/talks/%s" emacsconf-base-url emacsconf-year (org-entry-get (point) "SLUG"))) + ("duration" . ,(org-entry-get (point) "TIME"))))) + (while (string-match "\\${\\([-a-zA-Z_]+?\\)}" string start) + (if (assoc-default (match-string 1 string) values) + (setq string (replace-match (assoc-default (match-string 1 string) values) t t string)) + (setq string (replace-match (save-match-data (org-entry-get (point) (match-string 1 string))) t t string))) + (setq start (1+ (match-beginning 0)))) + string)) + +(defun emacsconf-mail-merge-format-email-address-for-subtree () + (if (string-match "," (org-entry-get (point) "EMAIL")) + (org-entry-get (point) "EMAIL") + (format "%s <%s>" (org-entry-get (point) "NAME") (org-entry-get (point) "EMAIL")))) + +(defun emacsconf-mail-merge-for-subtree (id note-field) + (let* ((template (emacsconf-mail-merge-get-template id)) + (body (emacsconf-mail-merge-fill (plist-get template :body))) + (subject (emacsconf-mail-merge-fill (plist-get template :subject))) + (note (org-entry-get (point) note-field))) + (compose-mail (emacsconf-mail-merge-format-email-address-for-subtree) + subject + `(("Reply-To" . ,(plist-get template :reply-to)) + ("Mail-Followup-To" . ,(plist-get template :mail-followup-to)) + ("Cc" . ,(plist-get template :cc)))) + (message-goto-body) + (save-excursion + (when note (insert "#+NOTE: " note "\n======== Delete above before sending =============\n\n")) + (insert body)))) + +(defun emacsconf-mail-merge-cancel () + (interactive) + (mapc (lambda (buffer) + (when (string-match "unsent" (buffer-name buffer)) + (let ((kill-buffer-query-functions nil)) + (set-buffer-modified-p nil) + (kill-buffer buffer)))) + (buffer-list))) + +(provide 'emacsconf-mail) +;;; emacsconf-mail.el ends here -- cgit v1.2.3