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.