EmacsConf backstage: capturing submissions from e-mails
| emacsconf, emacs, org2023-09-11: Updated code for recognizing fields.
People submit proposals for EmacsConf sessions via e-mail following
this submission template. (You can still submit a proposal until Sept 14!)
I mostly handle acceptance and scheduling, so I copy this information
into our private conf.org file so that we can use it to plan the draft
schedule, mail-merge speakers, and so on. I used to do this manually,
but I'm experimenting with using functions to create the heading
automatically so that it includes the date, talk title, and e-mail
address from the e-mail, and it calculates the notification date for
early acceptances as well. I use Notmuch for e-mail, so I can get the
properties from (notmuch-show-get-message-properties)
.
emacsconf-mail-add-submission: Add the submission from the current e-mail.
(defun emacsconf-mail-add-submission (slug) "Add the submission from the current e-mail." (interactive "MTalk ID: ") (let* ((props (notmuch-show-get-message-properties)) (from (or (plist-get (plist-get props :headers) :Reply-To) (plist-get (plist-get props :headers) :From))) (body (plist-get (car (plist-get props :body)) :content)) (date (format-time-string "%Y-%m-%d" (date-to-time (plist-get (plist-get props :headers) :Date)))) (to-notify (format-time-string "%Y-%m-%d" (time-add (days-to-time emacsconf-review-days) (date-to-time (plist-get (plist-get props :headers) :Date))))) (data (emacsconf-mail-parse-submission body))) (when (string-match "<\\(.*\\)>" from) (setq from (match-string 1 from))) (with-current-buffer (find-file emacsconf-org-file) ;; go to the submissions entry (goto-char (org-find-property "CUSTOM_ID" "submissions")) (when (org-find-property "CUSTOM_ID" slug) (error "Duplicate talk ID"))) (find-file emacsconf-org-file) (delete-other-windows) (outline-next-heading) (org-insert-heading) (insert " " (or (plist-get data :title) "") "\n") (org-todo "TO_REVIEW") (org-entry-put (point) "CUSTOM_ID" slug) (org-entry-put (point) "SLUG" slug) (org-entry-put (point) "TRACK" "General") (org-entry-put (point) "EMAIL" from) (org-entry-put (point) "DATE_SUBMITTED" date) (org-entry-put (point) "DATE_TO_NOTIFY" to-notify) (when (plist-get data :time) (org-entry-put (point) "TIME" (plist-get data :time))) (when (plist-get data :availability) (org-entry-put (point) "AVAILABILITY" (replace-regexp-in-string "\n+" " " (plist-get data :availability)))) (when (plist-get data :public) (org-entry-put (point) "PUBLIC_CONTACT" (replace-regexp-in-string "\n+" " " (plist-get data :public)))) (when (plist-get data :private) (org-entry-put (point) "EMERGENCY" (replace-regexp-in-string "\n+" " " (plist-get data :private)))) (when (plist-get data :q-and-a) (org-entry-put (point) "Q_AND_A" (replace-regexp-in-string "\n+" " " (plist-get data :q-and-a)))) (save-excursion (insert (plist-get data :body))) (re-search-backward org-drawer-regexp) (org-fold-hide-drawer-toggle 'off) (org-end-of-meta-data) (split-window-below)))
emacsconf-mail-parse-submission: Extract data from EmacsConf 2023 submissions in BODY.
(defun emacsconf-mail-parse-submission (body) "Extract data from EmacsConf 2023 submissions in BODY." (when (listp body) (setq body (plist-get (car body) :content))) (let ((data (list :body body)) (fields '((:title "^[* ]*Talk title") (:description "^[* ]*Talk description") (:format "^[* ]*Format") (:intro "^[* ]*Introduction for you and your talk") (:name "^[* ]*Speaker name") (:availability "^[* ]*Speaker availability") (:q-and-a "^[* ]*Preferred Q&A approach") (:public "^[* ]*Public contact information") (:private "^[* ]*Private emergency contact information") (:release "^[* ]*Please include this speaker release")))) (with-temp-buffer (insert body) (goto-char (point-min)) ;; Try to parse it (while fields ;; skip the field title (when (and (or (looking-at (cadar fields)) (re-search-forward (cadar fields) nil t)) (re-search-forward "\\(:[ \t\n]+\\|\n\n\\)" nil t)) ;; get the text between this and the next field (setq data (plist-put data (caar fields) (buffer-substring (point) (or (when (and (cdr fields) (re-search-forward (cadr (cadr fields)) nil t)) (goto-char (match-beginning 0)) (point)) (point-max)))))) (setq fields (cdr fields))) (if (string-match "[0-9]+" (or (plist-get data :format) "")) (plist-put data :time (match-string 0 (or (plist-get data :format) "")))) data)))
The functions above are in the emacsconf-el repository. When I call
emacsconf-mail-parse-submission
and give it the talk ID I want to
use, it makes the Org entry.
We store structured data in Org Mode properties such as NAME
,
EMAIL
and EMERGENCY
. I tend to make mistakes when typing, so I
have a short function that sets an Org property based on a region.
This is the code from my personal config:
my-org-set-property: In the current entry, set PROPERTY to VALUE.
(defun my-org-set-property (property value) "In the current entry, set PROPERTY to VALUE. Use the region if active." (interactive (list (org-read-property-name) (when (region-active-p) (replace-regexp-in-string "[ \n\t]+" " " (buffer-substring (point) (mark)))))) (org-set-property property value))
I've bound it to C-c C-x p
. This is what it looks like when I use it:
That helps me reduce errors in entering data. I sometimes forget details, so I ask other people to double-check my work, especially when it comes to speaker availability. That's how I copy the submission e-mails into our Org file.