forked from jkitchin/scimax
-
Notifications
You must be signed in to change notification settings - Fork 0
/
scimax-email.el
445 lines (383 loc) · 14 KB
/
scimax-email.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
;;; scimax-email.el --- Email functions -*- lexical-binding: t; -*-
;;; Commentary:
;;
;;; Code:
;; * Regular email functions
(require 'org-ref-export)
;;;###autoload
(defun email-region (start end)
"Send region as the body of an email."
(interactive "r")
(let* ((org-export-before-parsing-hook '((lambda (_)
(goto-char (point-min))
(unless (re-search-forward "bibliography:" nil t)
(goto-char (point-max))
(insert (format
"\nbibliography:%s"
(if (stringp bibtex-completion-bibliography)
bibtex-completion-bibliography
(string-join
bibtex-completion-bibliography ",")))))
(org-ref-csl-preprocess-buffer 'ascii))))
(org-export-show-temporary-export-buffer nil)
(content (progn
(org-ascii-export-as-ascii nil nil nil t)
(with-current-buffer "*Org ASCII Export*"
(buffer-string)))))
(compose-mail)
(message-goto-body)
(insert content)
(message-goto-to)))
;;;###autoload
(defun email-buffer ()
"Send buffer as the body of an email."
(interactive)
(email-region (point-min) (point-max)))
(defvar *email-heading-point* nil
"Global variable to store point in for returning.")
(defvar *email-to-addresses* nil
"Global variable to store to address in email.")
(defvar *email-mu4e-link-to-message* nil
"Global var to store mu4e link to Message-ID of last email.")
(defun email-send-action ()
"Send action for `compose-mail'."
(setq
*email-to-addresses*
(mapcar
'cadr
(mail-extract-address-components (mail-fetch-field "TO") t)))
(setq *email-mu4e-link-to-message*
(format "[[mu4e:msgid:%s][%s (%s)]]"
;; borrowed from https://github.com/girzel/gnorb/blob/master/gnorb-utils.el#L137
(replace-regexp-in-string
"\\(\\`<\\|>\\'\\)" "" (mail-fetch-field "Message-ID"))
(mail-fetch-field "Subject")
(current-time-string)))
(save-excursion
(switch-to-buffer (marker-buffer *email-heading-point*))
(goto-char (marker-position *email-heading-point*))
(when (not (org-at-heading-p))
(org-previous-visible-heading 1))
(setq *email-heading-point* nil)
(org-set-property "SENT-ON" (current-time-string))
;; reset this incase you added new ones
(org-set-property "TO" (mapconcat 'identity *email-to-addresses* ", "))
(org-set-property "Message-ID" *email-mu4e-link-to-message*)
;; remove unsent tag if it is there, and add sent
(let ((tags (org-get-tags)))
(add-to-list 'tags "sent")
(setq tags (-remove-item "unsent" tags))
(org-set-tags-to tags)))
(mu4e-update-mail-and-index t))
;;;###autoload
(defun email-heading (send)
"Send the current org-mode heading as the body of an email, with headline as the subject.
use these properties if they exist
TO
CC
BCC
SUBJECT
OTHER-HEADERS is an alist specifying additional
header fields. Elements look like (HEADER . VALUE) where both
HEADER and VALUE are strings.
with prefix arg SEND, send immediately.
Save when it was sent as a SENT property. this is overwritten on
subsequent sends."
(interactive "P")
; store location.
(setq *email-heading-point* (set-marker (make-marker) (point)))
(save-excursion
(org-mark-subtree)
(let* ((org-export-before-parsing-hook '((lambda (_)
(unless (re-search-forward "bibliography:" nil t)
(goto-char (point-max))
(insert (format "\nbibliography:%s"
(if (stringp bibtex-completion-bibliography)
bibtex-completion-bibliography
(string-join bibtex-completion-bibliography ","))))))
org-ref-csl-preprocess-buffer))
(content (org-export-string-as
(buffer-substring (point) (mark)) 'ascii t))
(TO (org-entry-get (point) "TO" t))
(CC (org-entry-get (point) "CC" t))
(BCC (org-entry-get (point) "BCC" t))
(SUBJECT (or (org-entry-get (point) "SUBJECT" t) (nth 4 (org-heading-components))))
(OTHER-HEADERS (read (or (org-entry-get (point) "OTHER-HEADERS") "()")))
(continue nil)
(switch-function nil)
(yank-action nil)
(send-actions '((email-send-action . nil))))
(compose-mail TO SUBJECT OTHER-HEADERS
continue switch-function yank-action
send-actions)
(message-goto-body)
(insert content)
(when CC
(message-goto-cc)
(insert CC))
(when BCC
(message-goto-bcc)
(insert BCC))
(if TO
(message-goto-body)
(message-goto-to))
(when send
(message-send-and-exit)))))
;;;###autoload
(defun email-heading-body (send)
"Send the current org-mode heading content as the body of an email.
Does not include the headline
Use these properties on the headline to create the email.
TO
CC
BCC
SUBJECT (or use the headline)
OTHER-HEADERS is an alist specifying additional
header fields. Elements look like (HEADER . VALUE) where both
HEADER and VALUE are strings.
With prefix arg SEND, sends immediately.
Save when it was sent as a SENT property on the headline. This is
overwritten on subsequent sends."
(interactive "P")
; store location.
(setq *email-heading-point* (set-marker (make-marker) (point)))
(save-excursion
(let* ((TO (org-entry-get (point) "TO" t))
(CC (org-entry-get (point) "CC" t))
(BCC (org-entry-get (point) "BCC" t))
(SUBJECT (or (org-entry-get (point) "SUBJECT" t)
(nth 4 (org-heading-components))))
(OTHER-HEADERS (eval (org-entry-get (point) "OTHER-HEADERS")))
(org-export-before-parsing-hook '((lambda (_)
(goto-char (point-min))
(unless (re-search-forward "bibliography:" nil t)
(goto-char (point-max))
(insert (format
"\nbibliography:%s"
(if (stringp bibtex-completion-bibliography)
bibtex-completion-bibliography
(string-join
bibtex-completion-bibliography ",")))))
(org-ref-csl-preprocess-buffer 'ascii))))
(content (progn
(unless (org-at-heading-p) (outline-previous-heading))
(let ((headline (org-element-at-point)))
(org-end-of-meta-data)
(save-restriction
(narrow-to-region (point)
(org-element-property :contents-end headline))
(org-ascii-export-as-ascii nil nil nil t)
(with-current-buffer "*Org ASCII Export*"
(buffer-string))))))
(continue nil)
(switch-function nil)
(yank-action nil)
(send-actions '((email-send-action . nil))))
(compose-mail TO SUBJECT OTHER-HEADERS continue switch-function yank-action send-actions)
(message-goto-body)
(insert content)
(when CC
(message-goto-cc)
(insert CC))
(when BCC
(message-goto-bcc)
(insert BCC))
(if TO
(message-goto-body)
(message-goto-to))
(when send
(message-send-and-exit)))))
;;;###autoload
(defun email-bibtex-entry ()
"Email bibtex entry/pdf that the cursor is in."
(interactive)
(save-excursion
(bibtex-beginning-of-entry)
(let* ((key (bibtex-completion-get-key-bibtex))
(pdf (bibtex-completion-find-pdf key)))
(bibtex-copy-entry-as-kill)
(compose-mail)
(message-goto-body)
(insert (pop bibtex-entry-kill-ring))
(message-goto-subject)
(insert (concat "Bibtex entry: " key))
(when (and pdf (file-exists-p pdf))
(mml-attach-file pdf))
(message-goto-to))))
;; * Mail-merge
;; mail-merge library for using org-mode, mu4e and email.el to send mail
;; merges. The idea is to write a mail-template that can be formatted by
;; `s-format', use emacs-lisp to generate a data-source that will populate each
;; template and generate an org-mode heading for each message using
;; `mail-merge-make-headings'. Then, you can review the messages, edit as
;; needed, and finally send them via `mail-merge'.
;;; Code:
(defun mail-merge-make-headings (s-template data-source)
"Create the mail headings.
S-TEMPLATE is an `s-format' string. DATA-SOURCE is an alist of
entries that will be used to expand the S-TEMPLATE and generate
the headings.
Each entry in DATA-SOURCE must contain \"TO\" which is the email
address(es) to send the message to. Also a \"SUBJECT\" must be
included, as well as a \"HEADLINE\" which will be used in the
headline instead of the subject.
The function will make a headline called Messages as a subheading
of the current heading, and each message will be a subheading of
the Messages heading.
an org-id will be created for each message. you can use ${ID} in
the S-TEMPLATE to refer to the ID property of the generated
message headline.
Each message will be tagged :unsent:
This function does not send the messages.
Example usage:
#+name: data
| TO | name | application-id | subject |
|--------------+------+----------------+------------|
| [email protected] | Bill | 123 | [J] person |
| [email protected] | John | 456 | [J] two |
#+BEGIN_SRC emacs-lisp :var d=data
(mail-merge-make-headings
\"Dear ${name},
Thank you for submitting application ${application-id}.
-----------------------
Please do not delete this.
[[id:${ID}]]
\"
(cl-loop for (to name id subject) in d collect
(list (cons \"TO\" to)
(cons \"name\" name)
(cons \"application-id\" id)
(cons \"SUBJECT\" subject))))
#+END_SRC
This only creates the messages. It does not send them.
See `mail-merge-send-heading' to send one heading (e.g. to test it).
See `mail-merge' to send them all.
See also the speed keys below to send each heading manually."
;; create Messages heading if needed
(save-restriction
(org-narrow-to-subtree)
(let ((this-id nil))
(save-excursion
(unless (and (outline-next-heading)
(string= "Messages" (nth 4 (org-heading-components))))
(org-insert-subheading nil)
(insert "Messages"))
(setq this-id (org-id-get-create)))
;; create Message entries
(cl-loop for data in data-source
do (save-excursion
(save-restriction
(org-narrow-to-subtree)
(goto-char (cdr (org-id-find this-id)))
(org-insert-heading-after-current)
(org-do-demote)
(setq data (add-to-list 'data
(cons "ID" (org-id-get-create))))
(outline-previous-heading)
(end-of-line)
(insert (or (cdr (assoc "HEADLINE" data))
(cdr (assoc "SUBJECT" data))))
(org-set-tags-to (-uniq (append '("unsent") (org-get-tags))))
(org-end-of-meta-data)
(insert (s-format s-template 'aget data))
;; refill now that it is expanded
(outline-previous-heading)
(save-restriction
(org-narrow-to-subtree)
(goto-char (point-min))
(fill-region (point-min) (or (re-search-forward "^--"
nil t)
(point-max))))
(org-entry-put (point) "TO" (cdr (assoc "TO" data)))
(when (cdr (assoc "SUBJECT" data))
(org-entry-put (point) "SUBJECT" (cdr (assoc "SUBJECT" data))))))))))
;;;###autoload
(defun mail-merge-send-heading (&optional just-send)
"Create message with org-heading body at point using heading properties.
With prefix arg, also send the message and move to the next one."
(interactive "P")
(setq *email-heading-point* (set-marker (make-marker) (point)))
(save-excursion
(let ((content (progn
(unless (org-at-heading-p) (outline-previous-heading))
(let ((headline (org-element-at-point)))
(buffer-substring
(progn (org-end-of-meta-data t) (point))
(org-element-property :contents-end headline)))))
(TO (org-entry-get (point) "TO" t))
(CC (org-entry-get (point) "CC" t))
(BCC (org-entry-get (point) "BCC" t))
(SUBJECT (replace-regexp-in-string
"{{.*}} "
""
(or (org-entry-get (point) "SUBJECT" t)
(nth 4 (org-heading-components)))))
(OTHER-HEADERS (eval (org-entry-get (point) "OTHER-HEADERS")))
(continue nil)
(switch-function nil)
(yank-action nil)
(send-actions '((email-send-action . nil))))
(compose-mail TO SUBJECT OTHER-HEADERS continue switch-function
yank-action send-actions)
(message-goto-body)
(insert content)
(when CC
(message-goto-cc)
(insert CC))
(when BCC
(message-goto-bcc)
(insert BCC))
;; move point back to the top
(message-goto-to)
(when just-send
(message-send-and-exit))))
(org-todo "DONE")
(let ((tags (-remove
(lambda (x) (string= x "unsent"))
(org-get-tags))))
(add-to-list 'tags "sent")
(org-set-tags-to tags))
(message (format "sent to %s" (org-entry-get (point) "TO")))
(outline-hide-entry)
(outline-next-heading)
(outline-show-entry))
;;;###autoload
(defun mail-merge ()
"Run a mail-merge in the current heading.
This will map over entries tagged unsent with a TO property, and
mail the body of each heading using
`mail-merge-send-heading'. Headings tagged ignore will be ignored."
(interactive)
(org-map-entries
(lambda ()
(mail-merge-send-heading t)
(sleep-for 0.2))
;; on headings that are tagged unsent
"unsent-ignore+TO={.}"))
;; ** Speed commands for mail-merge
(defun org-speed-mail-merge (keys)
"Find the command to run for KEYS."
(when (or (and (bolp) (looking-at org-outline-regexp)
(not (null (org-entry-get (point) "TO")))))
(cdr (assoc keys org-speed-commands-mail-merge))))
(defun mail-merge-speed-key-help ()
"Print speed key help."
(with-output-to-temp-buffer "*Help*"
(princ "Mail merge speed commands\n==========================\n")
(mapc #'org-print-speed-command org-speed-commands-mail-merge)
(princ "\n")
(princ "User-defined Speed commands\n===========================\n")
(mapc #'org-print-speed-command org-speed-commands-user)
(princ "Built-in Speed commands\n=======================\n")
(mapc #'org-print-speed-command org-speed-commands-default))
(with-current-buffer "*Help*"
(setq truncate-lines t)))
;; Should I make a mail-merge speed command, M? or should that always require
;; thinking. I lean towards thinking.
(setq org-speed-commands-mail-merge
'(("m" . (mail-merge-send-heading))
("s" . (mail-merge-send-heading t))
("?" . mail-merge-speed-key-help)))
(add-hook 'org-speed-command-hook 'org-speed-mail-merge)
(provide 'scimax-email)
;;; scimax-email.el ends here